diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 6ec53cd2..89ccfcf1 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -33,6 +33,10 @@ jobs: test -x ./headless-ctl || (echo "headless-ctl not built" && exit 1) echo "Build outputs verified successfully" + - name: Run headless test suites + run: | + make check + - name: Run basic headless test run: | # Start demo in background, capture stderr for memory stats @@ -121,6 +125,7 @@ jobs: - name: Memory leak detection with Valgrind run: | + export DEBUGINFOD_URLS= # Run with Valgrind for leak detection timeout 15s valgrind \ --leak-check=full \ @@ -157,3 +162,53 @@ jobs: exit 1 fi echo "No definite memory leaks detected" + + - name: Rebuild with the mobile window manager + run: | + python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y + python3 tools/kconfig/genconfig.py configs/Kconfig + make -j$(nproc) + make check + + - name: Phone end-to-end + run: | + # Boot the phone shell, unlock, open Calculator, type, screenshot + timeout 30s ./demo-headless 2>phone_stderr.log & + PID=$! + sleep 2 + ./headless-ctl shot phone_lock.png + # Slide to unlock: drag the knob along the track + ./headless-ctl mouse down 33 295 1 + for x in 60 100 140 180 220 230; do + ./headless-ctl mouse move $x 295 1 + sleep 0.05 + done + ./headless-ctl mouse up 230 295 1 + sleep 1 + ./headless-ctl shot phone_home.png + # Calculator icon (second row, first column) + ./headless-ctl mouse down 40 128 1 + ./headless-ctl mouse up 40 128 1 + sleep 1 + ./headless-ctl type "12+30=" + sleep 1 + ./headless-ctl shot phone_calc.png + ./headless-ctl shutdown || true + wait $PID || true + for f in phone_lock.png phone_home.png phone_calc.png; do + identify "$f" > /dev/null || (echo "$f invalid" && exit 1) + done + # The three screens must differ + for a in "phone_lock.png phone_home.png" "phone_home.png phone_calc.png"; do + set -- $a + DIFF=$(compare -metric AE $1 $2 null: 2>&1 | grep -oE '^[0-9]+' | head -1) + echo "$1 vs $2: $DIFF pixels differ" + [ "${DIFF:-0}" -gt 1000 ] || (echo "navigation did not change the screen" && exit 1) + done + + - name: Upload phone screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: phone-screenshots + path: phone_*.png diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3197d255..9fa52dd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,12 +16,15 @@ jobs: with: files: | .ci/** + .github/workflows/** mk/** include/** src/** backend/** apps/** tools/** + tests/** + configs/** .clang-format Makefile - name: Set has_code_related_changes @@ -44,10 +47,104 @@ jobs: sudo apt-get update -q -y sudo apt-get install -y libsdl2-dev libjpeg-dev libpng-dev libcairo2-dev shell: bash - - name: default build + - name: default build (desktop window manager) run: | make defconfig make + - name: headless tests + run: | + make check + - name: headless tests under Valgrind + run: | + sudo apt-get install -y -q valgrind + for t in test-core test-wm test-lifetime test-demos; do + DEBUGINFOD_URLS= valgrind -q --error-exitcode=1 \ + --leak-check=full --errors-for-leak-kinds=definite ./$t + done + shell: bash + - name: mobile build (phone and demos) + run: | + python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y + python3 tools/kconfig/genconfig.py configs/Kconfig + make + - name: headless tests (mobile) + run: | + make check + - name: headless tests under Valgrind (mobile) + run: | + for t in test-core test-wm test-lifetime test-demos test-cellphone; do + DEBUGINFOD_URLS= valgrind -q --error-exitcode=1 \ + --leak-check=full --errors-for-leak-kinds=definite ./$t + done + shell: bash + + sanitizers: + needs: [detect-code-related-file-changes] + if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: install-dependencies + run: | + sudo apt-get update -q -y + sudo apt-get install -y libsdl2-dev libjpeg-dev libpng-dev + - name: AddressSanitizer + UndefinedBehaviorSanitizer + run: | + for wm in WM_DESKTOP WM_MOBILE WM_NONE; do + make defconfig + python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig $wm=y + python3 tools/kconfig/genconfig.py configs/Kconfig + make clean + make check \ + CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined" \ + LDFLAGS="-fsanitize=address,undefined" + done + shell: bash + + config-matrix: + needs: [detect-code-related-file-changes] + if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - name: no window manager + options: WM_NONE=y + - name: no coverage cache + options: WM_MOBILE=y CACHE_SIZE=0 + - name: no mobile eviction + options: WM_MOBILE=y WM_MOBILE_EVICT=n + - name: no shadows + options: DROP_SHADOW=n + - name: no allocator stats + options: MEMORY_STATS=n + - name: no widget toolkit + options: WM_MOBILE=y WIDGETS=n + - name: RGB565 phone surfaces + options: WM_MOBILE=y DEMO_CELLPHONE_RGB16=y + - name: TLSF heap + options: WM_MOBILE=y MEM_TLSF=y MEM_TLSF_POOL_SIZE=4194304 + - name: Pixman renderer + options: WM_MOBILE=y RENDERER_PIXMAN=y + - name: Pixman renderer, desktop + options: RENDERER_PIXMAN=y + - name: size-optimized, heap masks only + options: WM_MOBILE=y OPTIMIZE_SIZE=y SCRATCH_SIZE=0 + name: config (${{ matrix.name }}) + steps: + - uses: actions/checkout@v6 + - name: install-dependencies + run: | + sudo apt-get update -q -y + sudo apt-get install -y libsdl2-dev libjpeg-dev libpng-dev libpixman-1-dev + - name: build and test + run: | + make defconfig + python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig ${{ matrix.options }} + python3 tools/kconfig/genconfig.py configs/Kconfig + make + make check coding-style: needs: [detect-code-related-file-changes] diff --git a/.gitignore b/.gitignore index 330d3c48..d2afd883 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,14 @@ demo-* .demo-* mado-perf +test-* +!tests/test-*.c +.test-* +*.fail.ppm .mado-perf font-edit .font-edit src/composite-decls.h -src/shadow-gaussian-lut.h # Swap [._]*.s[a-v][a-z] diff --git a/Makefile b/Makefile index 0d615821..7db22b8f 100644 --- a/Makefile +++ b/Makefile @@ -43,37 +43,50 @@ libtwin.a_cflags-y := libtwin.a_cflags-$(CC_IS_EMCC) += -Oz libtwin.a_files-y = \ - src/box.c \ src/poly.c \ - src/toplevel.c \ - src/button.c \ src/fixed.c \ - src/label.c \ src/trig.c \ src/convolve.c \ src/font.c \ + src/text.c \ + src/cache.c \ src/matrix.c \ src/queue.c \ - src/widget.c \ src/font_default.c \ src/path.c \ src/screen.c \ src/window.c \ - src/dispatch.c \ + src/wm.c \ src/geom.c \ src/pattern.c \ src/spline.c \ src/work.c \ - src/draw-common.c \ src/hull.c \ - src/icon.c \ src/pixmap.c \ src/timeout.c \ + src/anim.c \ src/image.c \ src/animation.c \ src/closure.c \ src/api.c +# Window manager policy: exactly one is built +libtwin.a_files-$(CONFIG_WM_NONE) += src/wm-none.c +libtwin.a_files-$(CONFIG_WM_DESKTOP) += src/wm-desktop.c src/icon.c +libtwin.a_files-$(CONFIG_WM_MOBILE) += src/wm-mobile.c + +# Box layout toolkit +libtwin.a_files-$(CONFIG_WIDGETS) += \ + src/widget.c \ + src/box.c \ + src/label.c \ + src/button.c \ + src/toplevel.c + +# Fonts +libtwin.a_files-$(CONFIG_FONT_MONTSERRAT) += src/font-montserrat.c +libtwin.a_files-$(CONFIG_FONT_ICONS) += src/font-icons.c + libtwin.a_includes-y := \ include \ src @@ -83,10 +96,6 @@ src/composite-decls.h: scripts/gen-composite-decls.py @echo " GEN $@" @$< > $@ -src/shadow-gaussian-lut.h: scripts/gen-shadow-lut.py - @echo " GEN $@" - @./scripts/gen-shadow-lut.py $@ - # Optional features libtwin.a_files-$(CONFIG_LOGGING) += src/log.c @@ -142,6 +151,7 @@ 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 @@ -149,7 +159,29 @@ 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_includes-y := include +libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ + apps/cellphone/ui.c \ + apps/cellphone/ui-paint.c \ + apps/cellphone/ui-input.c \ + 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 + +libapps.a_includes-y := include apps apps/cellphone # Emscripten size optimization libapps.a_cflags-$(CC_IS_EMCC) += -Oz @@ -204,7 +236,7 @@ ifeq ($(CONFIG_PERF_TEST), y) target-$(CONFIG_PERF_TEST) += mado-perf mado-perf_depends-y += $(target.a-y) mado-perf_files-y += tools/perf.c -mado-perf_includes-y := include +mado-perf_includes-y := include src mado-perf_ldflags-y := \ $(target.a-y) \ $(TARGET_LIBS) @@ -216,7 +248,7 @@ ifeq ($(CONFIG_DEMO_APPLICATIONS), y) target-y += demo-$(BACKEND) demo-$(BACKEND)_depends-y += $(target.a-y) demo-$(BACKEND)_files-y = apps/main.c -demo-$(BACKEND)_includes-y := include +demo-$(BACKEND)_includes-y := include apps/cellphone # Emscripten size optimization demo-$(BACKEND)_cflags-$(CC_IS_EMCC) += -Oz demo-$(BACKEND)_ldflags-y := \ @@ -280,9 +312,55 @@ headless-ctl_ldflags-y := # -lrt endif 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) +TESTS += test-demos +endif +ifeq ($(CONFIG_DEMO_CELLPHONE), y) +TESTS += test-cellphone +endif + +ifneq ($(filter check check-update clean distclean,$(MAKECMDGOALS)),) +ifneq ($(CC_IS_EMCC), 1) +define test-target +target-y += $(1) +$(1)_depends-y += $(target.a-y) +$(1)_files-y := tests/$(1).c tests/harness.c \ + $(if $(filter test-cellphone,$(1)),$(wildcard tests/phone-*.c)) +$(1)_includes-y := include src tests apps apps/cellphone +$(1)_ldflags-y := $(target.a-y) $(TARGET_LIBS) +endef +$(foreach t,$(TESTS),$(eval $(call test-target,$(t)))) +endif +endif + +# Goals that build targets, so header dependencies apply to them too +extra-build-goals := check check-update + +.PHONY: check check-update +ifeq ($(CC_IS_EMCC), 1) +check check-update: + @echo "The headless tests run natively; rebuild without Emscripten" >&2 + @exit 1 +else +check: $(TESTS) + @mkdir -p tests/golden + @status=0; for t in $(TESTS); do ./$$t || status=1; done; exit $$status + +check-update: $(TESTS) + @mkdir -p tests/golden + @status=0; for t in $(TESTS); do MADO_UPDATE_GOLDEN=1 ./$$t || status=1; done; exit $$status +endif + # Build system integration -CFLAGS += -include config.h +override CFLAGS += -include config.h +ifeq ($(CONFIG_OPTIMIZE_SIZE), y) +# After -O2 in the common flags, so it wins +override CFLAGS += -Os -fno-asynchronous-unwind-tables -fno-unwind-tables +endif # Ensure composite-decls.h exists before including build rules # (needed for dependency generation in mk/common.mk) @@ -290,9 +368,6 @@ ifeq ($(filter config defconfig clean distclean,$(MAKECMDGOALS)),) ifeq ($(wildcard src/composite-decls.h),) $(shell scripts/gen-composite-decls.py > src/composite-decls.h) endif - ifeq ($(wildcard src/shadow-gaussian-lut.h),) - $(shell ./scripts/gen-shadow-lut.py src/shadow-gaussian-lut.h) - endif endif # Only skip build rules when running ONLY config/defconfig (no other targets) diff --git a/README.md b/README.md index bd6829c5..890fd847 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,38 @@ Run the demo program: $ ./demo-sdl ``` -Once the window appears, you can move the windows and interact with the widgets. +The default build uses the desktop window manager: overlapping, decorated +windows running the demo apps. 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 +fixtures) on the mobile window manager; the launcher also lists every demo +app that is built: + +```shell +$ make defconfig +$ python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y +$ python3 tools/kconfig/genconfig.py configs/Kconfig +$ make +$ ./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 +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. + +### Verification + +Headless test suites run in-process with a virtual clock. At checkpoints +they check the incrementally updated frame against a full recomposite, and +the core and phone suites also compare golden frame checksums: + +```shell +$ make check # core, window manager, lifetime (and phone) suites +$ make check-update # re-record golden frame checksums +``` For information about other backends (Linux framebuffer, VNC, headless), see [docs/backends.md](docs/backends.md). diff --git a/apps/animation.c b/apps/animation.c index ebc98906..2d3ff463 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -8,7 +8,7 @@ #include -#include "apps_animation.h" +#include "apps.h" static inline twin_pixmap_t *_apps_animation_pixmap( twin_custom_widget_t *animation) @@ -27,20 +27,27 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) (apps_animation_data_t *) twin_custom_widget_data(custom); twin_pixmap_t *current_frame = NULL; - if (twin_pixmap_is_animated(anim->pix)) { - twin_animation_t *a = anim->pix->animation; - current_frame = twin_animation_get_current_frame(a); - twin_animation_advance_frame(a); - } else { - current_frame = anim->pix; - } + /* 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; + + /* 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 */ + 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); + twin_coord_t fw = twin_pixmap_width(current_frame); + twin_coord_t fh = twin_pixmap_height(current_frame); + twin_coord_t x = w > fw ? (w - fw) / 2 : 0; + twin_coord_t y = h > fh ? (h - fh) / 2 : 0; twin_operand_t srcop = { .source_kind = TWIN_PIXMAP, .u.pixmap = current_frame, }; - twin_composite(_apps_animation_pixmap(custom), 0, 0, &srcop, 0, 0, NULL, 0, - 0, TWIN_SOURCE, current_frame->width, current_frame->height); + twin_composite(dst, x, y, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, fw, fh); } static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) @@ -49,26 +56,30 @@ static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) twin_custom_widget_t *custom = closure; apps_animation_data_t *anim = (apps_animation_data_t *) twin_custom_widget_data(custom); + twin_animation_t *a = twin_pixmap_animation(anim->pix); + twin_animation_advance_frame(a); twin_custom_widget_queue_paint(custom); - twin_animation_t *a = anim->pix->animation; - twin_time_t delay = twin_animation_get_current_delay(a); - return delay; + return twin_animation_get_current_delay(a); } -static twin_dispatch_result_t _apps_animation_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t _apps_animation_dispatch( + twin_custom_widget_t *custom, + twin_event_t *event) { - (void) closure; /* unused parameter */ - - twin_custom_widget_t *custom = twin_widget_get_custom(widget); - if (!custom) - return TwinDispatchContinue; - switch (event->kind) { case TwinEventPaint: _apps_animation_paint(custom); break; + case TwinEventDestroy: { + /* The pixmap owns the animation, so this frees every frame too */ + apps_animation_data_t *anim = + (apps_animation_data_t *) twin_custom_widget_data(custom); + twin_clear_timeout(anim->timeout); + anim->timeout = NULL; + twin_pixmap_destroy(anim->pix); + anim->pix = NULL; + break; + } default: break; } @@ -79,7 +90,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, 0, 0, 0, 1, 1, _apps_animation_dispatch, + parent, 0xff000000, 0, 0, _apps_animation_dispatch, sizeof(apps_animation_data_t)); if (!custom) return NULL; @@ -88,8 +99,8 @@ static twin_custom_widget_t *_apps_animation_init(twin_box_t *parent, (apps_animation_data_t *) twin_custom_widget_data(custom); anim->pix = pix; - if (twin_pixmap_is_animated(anim->pix)) { - twin_animation_t *a = anim->pix->animation; + twin_animation_t *a = twin_pixmap_animation(anim->pix); + if (a) { twin_time_t delay = twin_animation_get_current_delay(a); anim->timeout = twin_set_timeout(_apps_animation_timeout, delay, custom); @@ -106,17 +117,30 @@ static twin_custom_widget_t *apps_animation_create(twin_box_t *parent, return _apps_animation_init(parent, pix); } -void apps_animation_start(twin_screen_t *screen, +/* Sized by the image: @w and @h are ignored */ +bool apps_animation_start(twin_screen_t *screen, const char *name, - const char *path, int x, - int y) + int y, + int w, + int h) { - twin_pixmap_t *pix = twin_pixmap_from_file(path, TWIN_ARGB32); - twin_toplevel_t *toplevel = - twin_toplevel_create(screen, TWIN_ARGB32, TwinWindowApplication, x, y, - pix->width, pix->height, name); - twin_custom_widget_t *anim = apps_animation_create(&toplevel->box, pix); - (void) anim; + (void) w, (void) h; + twin_pixmap_t *pix = twin_pixmap_from_file(ASSET_PATH "nyancat.gif"); + if (!pix) + return false; + twin_toplevel_t *toplevel = twin_toplevel_create( + screen, x, y, twin_pixmap_width(pix), twin_pixmap_height(pix), name); + if (!toplevel) { + twin_pixmap_destroy(pix); + return false; + } + /* The widget owns the image from here on */ + if (!apps_animation_create(twin_toplevel_box(toplevel), pix)) { + twin_toplevel_destroy(toplevel); + twin_pixmap_destroy(pix); + return false; + } twin_toplevel_show(toplevel); + return true; } diff --git a/apps/apps.c b/apps/apps.c new file mode 100644 index 00000000..796f0c86 --- /dev/null +++ b/apps/apps.c @@ -0,0 +1,55 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include "apps.h" + +#if defined(CONFIG_DEMO_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) + /* 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) + {"Demo", TWIN_SYM_LIST, 100, 100, 400, 400, apps_multi_start, NULL, NULL}, +#endif +#if defined(CONFIG_DEMO_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}, +#endif +#if defined(CONFIG_DEMO_SPLINE) + {"Spline", TWIN_SYM_SHUFFLE, 20, 20, 400, 400, apps_spline_start, NULL, + NULL}, +#endif +#if defined(CONFIG_DEMO_ANIMATION) + {"Animation", TWIN_SYM_PLAY, 20, 20, 0, 0, apps_animation_start, NULL, + NULL}, +#endif +#if defined(CONFIG_DEMO_IMAGE) + {"Vectors", TWIN_SYM_GLOBE, 20, 20, 0, 0, apps_image_start, NULL, NULL}, +#endif + {0}, /* keeps the table valid when nothing is built */ +}; + +#define N_APPS ((int) (sizeof(apps) / sizeof(apps[0])) - 1) + +int apps_count(void) +{ + return N_APPS; +} + +const apps_entry_t *apps_at(int idx) +{ + return idx >= 0 && idx < N_APPS ? &apps[idx] : NULL; +} diff --git a/apps/apps.h b/apps/apps.h new file mode 100644 index 00000000..9e004743 --- /dev/null +++ b/apps/apps.h @@ -0,0 +1,53 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * 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. + * + * 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 + * its entry is compiled in only when it can run. + */ + +#ifndef _APPS_H_ +#define _APPS_H_ + +#include +#include + +/* Open the app's windows on @screen at @x,@y, @w x @h titled @name (the + * policy may place them elsewhere); false when it could not start */ +typedef bool apps_start_fn(twin_screen_t *screen, + const char *name, + int x, + int y, + int w, + int h); + +typedef struct apps_entry { + const char *name; /* window title and launcher label */ + const char *icon; /* TWIN_SYM_* shown by launchers */ + int x, y, w, h; /* placement requested on a desktop; a shell's screen */ + apps_start_fn *start; + /* A shell takes over a screen of its own w x h and launches the other + * apps itself (the phone): it has boot and stop instead of start */ + bool (*boot)(twin_screen_t *screen); + void (*stop)(void); +} apps_entry_t; + +int apps_count(void); +const apps_entry_t *apps_at(int idx); + +/* Bundled images and vectors, relative to the working directory */ +#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; + +#endif /* _APPS_H_ */ diff --git a/apps/apps_animation.h b/apps/apps_animation.h deleted file mode 100644 index b6b88829..00000000 --- a/apps/apps_animation.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2024 National Cheng Kung University - * All rights reserved. - */ - -#ifndef _APPS_ANIMATION_H_ -#define _APPS_ANIMATION_H_ - -#include - -void apps_animation_start(twin_screen_t *screen, - const char *name, - const char *path, - int x, - int y); - -#endif /* _APPS_ANIMATION_H_ */ diff --git a/apps/apps_calc.h b/apps/apps_calc.h deleted file mode 100644 index 923de7da..00000000 --- a/apps/apps_calc.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_CALC_H_ -#define _APPS_CALC_H_ - -#include - -void apps_calc_start(twin_screen_t *screen, - const char *name, - int x, - int y, - int w, - int h); - -#endif /* _APPS_CALC_H_ */ diff --git a/apps/apps_clock.h b/apps/apps_clock.h deleted file mode 100644 index 71518c8a..00000000 --- a/apps/apps_clock.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_CLOCK_H_ -#define _APPS_CLOCK_H_ - -#include - -void apps_clock_start(twin_screen_t *screen, - const char *name, - int x, - int y, - int w, - int h); - -#endif /* _APPS_CLOCK_H_ */ diff --git a/apps/apps_image.h b/apps/apps_image.h deleted file mode 100644 index 06320d5f..00000000 --- a/apps/apps_image.h +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2024 National Cheng Kung University - * All rights reserved. - */ - -#ifndef _APPS_IMAGE_H_ -#define _APPS_IMAGE_H_ - -#include - -void apps_image_start(twin_screen_t *screen, const char *name, int x, int y); - -#endif /* _APPS_ANIMATION_H_ */ diff --git a/apps/apps_multi.h b/apps/apps_multi.h deleted file mode 100644 index b4097b74..00000000 --- a/apps/apps_multi.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_MULTI_H_ -#define _APPS_MULTI_H_ - -#include - -void apps_multi_start(twin_screen_t *screen, - const char *name, - int x, - int y, - int w, - int h); - -#endif /* _APPS_MULTI_H_ */ diff --git a/apps/apps_spline.h b/apps/apps_spline.h deleted file mode 100644 index a527a20b..00000000 --- a/apps/apps_spline.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_SPLINE_H_ -#define _APPS_SPLINE_H_ - -#include - -void apps_spline_start(twin_screen_t *screen, - const char *name, - int x, - int y, - int w, - int h); - -#endif /* _APPS_SPLINE_H_ */ diff --git a/apps/calc.c b/apps/calc.c index aa433cf6..4cab58ef 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -7,7 +7,7 @@ #include #include -#include "apps_calc.h" +#include "apps.h" #define APPS_CALC_STACK 5 @@ -75,6 +75,9 @@ static const char *apps_calc_labels[] = { #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 @@ -94,8 +97,7 @@ static void _apps_calc_update_value(apps_calc_t *calc) char v[20]; sprintf(v, "%d", calc->stack[0]); - twin_label_set(calc->display, v, APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, - APPS_CALC_VALUE_STYLE); + twin_label_set(calc->display, v); } static void _apps_calc_push(apps_calc_t *calc) @@ -196,7 +198,18 @@ static twin_dispatch_result_t _apps_calc_button_clicked(twin_widget_t *widget, return TwinDispatchDone; } -void apps_calc_start(twin_screen_t *screen, +/* 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) +{ + (void) widget; + if (event->kind == TwinEventDestroy) + free(data); + return TwinDispatchContinue; +} + +bool apps_calc_start(twin_screen_t *screen, const char *name, int x, int y, @@ -204,37 +217,63 @@ void apps_calc_start(twin_screen_t *screen, int h) { apps_calc_t *calc = malloc(sizeof(apps_calc_t)); + if (!calc) + return false; - calc->toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + calc->toplevel = twin_toplevel_create(screen, x, y, w, h, name); + if (!calc->toplevel) { + free(calc); + 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(&calc->toplevel->box, "0", APPS_CALC_VALUE_FG, - APPS_CALC_VALUE_SIZE, APPS_CALC_VALUE_STYLE); - twin_widget_set(&calc->display->widget, APPS_CALC_VALUE_BG); - calc->display->align = TwinAlignRight; - calc->display->widget.shape = TwinShapeLozenge; - calc->keys = twin_box_create(&calc->toplevel->box, TwinBoxHorz); + 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); - twin_widget_set(&calc->buttons[b]->label.widget, - APPS_CALC_BUTTON_BG); - twin_widget_set_callback(&calc->buttons[b]->label.widget, - _apps_calc_button_clicked, calc); - calc->buttons[b]->label.widget.shape = TwinShapeEllipse; + 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) - calc->buttons[b]->label.widget.copy_geom = - &calc->buttons[calc_layout[0][0]]->label.widget; + twin_widget_match_size( + key, twin_button_widget(calc->buttons[calc_layout[0][0]])); } } + /* 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; + 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; + +fail: + twin_toplevel_destroy(calc->toplevel); + free(calc); + return false; } diff --git a/apps/cellphone/calc.c b/apps/cellphone/calc.c new file mode 100644 index 00000000..eb64160e --- /dev/null +++ b/apps/cellphone/calc.c @@ -0,0 +1,425 @@ +/* + * 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/calllog.c b/apps/cellphone/calllog.c new file mode 100644 index 00000000..aa1556b0 --- /dev/null +++ b/apps/cellphone/calllog.c @@ -0,0 +1,221 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Call Log: recent calls, newest first. Tapping a row calls back. + * + * Rows are painted straight from the const data table at their scrolled + * position; scrolling blits and repaints only the exposed band. + */ + +#include +#include + +#include "data.h" +#include "phone.h" + +#define ROW_H PHONE_LIST_ROW_H +#define LIST_TOP PHONE_HEADER_H +#define ICON_CX 20 + +typedef struct { + ui_view_t view; + ui_scroll_t scroll; + int pressed; /* row index, -1 when none */ + char missed[16]; +} calllog_t; + +static void calllog_layout(ui_view_t *v) +{ + calllog_t *l = (calllog_t *) v; + ui_scroll_setup(&l->scroll, + ui_rect(0, LIST_TOP, PHONE_W, ui_view_height(v) - LIST_TOP), + PHONE_CALLLOG_COUNT * ROW_H); +} + +static ui_rect_t row_rect(calllog_t *l, int i) +{ + return ui_rect(0, LIST_TOP + i * ROW_H - l->scroll.offset, PHONE_W, ROW_H); +} + +/* + * Direction glyph: an arrow into the call (incoming, missed) or out of it. + * Shape tells in from out even where the theme makes their colors equal. + */ +static void paint_direction(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + phone_call_dir_t dir, + twin_argb32_t color) +{ + /* Tail top-right, tip bottom-left; outgoing swaps them */ + twin_coord_t tx = cx - 5, ty = cy + 5, fx = cx + 5, fy = cy - 5; + int s = 1; + if (dir == PHONE_CALL_OUT) { + tx = cx + 5, ty = cy - 5, fx = cx - 5, fy = cy + 5; + s = -1; + } + ui_line(px, fx, fy, tx, ty, 2, color); + ui_line(px, tx, ty, tx + 7 * s, ty, 2, color); + ui_line(px, tx, ty, tx, ty - 7 * s, 2, color); +} + +static void paint_row(twin_pixmap_t *px, + calllog_t *l, + int i, + const phone_call_t *call) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = row_rect(l, i); + bool missed = call->dir == PHONE_CALL_MISSED; + twin_argb32_t accent = missed ? th->missed_red + : call->dir == PHONE_CALL_IN ? th->call_green + : th->primary; + + ui_fill(px, r, + i == l->pressed ? ui_mix(th->card, th->primary, 70) : th->card); + twin_coord_t cy = (r.top + r.bottom) / 2; + ui_circle(px, ICON_CX, cy, 14, ui_mix(th->card, accent, 40)); + paint_direction(px, ICON_CX, cy, call->dir, accent); + + /* "Today, 14:22": day over time in a right-hand column */ + char day[24]; + const char *comma = strchr(call->time, ','); + const char *clock = comma ? comma + 1 : ""; + size_t n = comma ? (size_t) (comma - call->time) : strlen(call->time); + if (n >= sizeof(day)) + n = sizeof(day) - 1; + memcpy(day, call->time, n); + day[n] = 0; + while (*clock == ' ') + clock++; + twin_coord_t right = r.right - 10; + twin_coord_t dw = ui_text_width(FONT_SM, day); + ui_text(px, FONT_SM, right, r.top + 17, UI_RIGHT, th->text_sec, day); + ui_text(px, FONT_SM, right, r.top + 32, UI_RIGHT, th->text_sec, clock); + + twin_coord_t x = ICON_CX + 18; + ui_rect_t text = ui_rect(x - 4, r.top, right - dw - 6 - x, ROW_H); + ui_text_fit(px, FONT_NORMAL, + ui_rect(text.left, r.top + 3, ui_w(text), ROW_H / 2 - 1), + UI_LEFT, missed ? th->missed_red : th->text, call->name); + ui_text_fit( + px, FONT_SM, + ui_rect(text.left, r.top + ROW_H / 2 - 1, ui_w(text), ROW_H / 2 - 2), + UI_LEFT, th->text_sec, call->phone); + ui_separator(px, ui_rect(x, r.top, PHONE_W - x, ROW_H)); +} + +static void calllog_paint(ui_view_t *v, twin_pixmap_t *px) +{ + calllog_t *l = (calllog_t *) v; + const phone_theme_t *th = phone_theme(); + const phone_call_t *log = phone_calllog(); + ui_rect_t clip = ui_view_clip(v); + + /* Rows first: the header paints over any row scrolled beneath it */ + for (int i = 0; i < PHONE_CALLLOG_COUNT; i++) { + ui_rect_t r = row_rect(l, i); + if (r.bottom <= LIST_TOP) + continue; + if (r.top >= ui_view_height(v)) + break; + if (ui_intersects(r, clip)) + paint_row(px, l, i, &log[i]); + } + ui_rect_t below = + ui_rect(0, LIST_TOP + PHONE_CALLLOG_COUNT * ROW_H - l->scroll.offset, + PHONE_W, ui_view_height(v)); + if (ui_intersects(below, clip)) + ui_fill(px, below, th->bg); + ui_scroll_bar(px, &l->scroll); + + ui_rect_t head = ui_rect(0, 0, PHONE_W, LIST_TOP); + if (ui_intersects(head, clip)) + ui_header(px, head, NULL, v->title, l->missed[0] ? l->missed : NULL); +} + +static int row_at(calllog_t *l, twin_coord_t x, twin_coord_t y) +{ + if (!ui_hit(l->scroll.area, x, y)) + return -1; + int i = (y - LIST_TOP + l->scroll.offset) / ROW_H; + return i >= 0 && i < PHONE_CALLLOG_COUNT ? i : -1; +} + +static void invalidate_row(calllog_t *l, int i) +{ + if (i >= 0) + ui_view_invalidate(&l->view, row_rect(l, i)); +} + +static ui_rect_t view_row(ui_view_t *v, int i) +{ + return row_rect((calllog_t *) v, i); +} + +static void set_pressed(calllog_t *l, int i) +{ + ui_view_set_pressed(&l->view, &l->pressed, i, view_row); +} + +static bool calllog_event(ui_view_t *v, const ui_event_t *e) +{ + calllog_t *l = (calllog_t *) v; + int was = l->pressed; + /* + * A drag lifts the highlight first. Its repaint may still be pending + * when the blit moves the row, so repaint the row where it lands too. + */ + if (e->kind == UI_DRAG || e->kind == UI_RELEASE) + set_pressed(l, -1); + if (ui_scroll_event(v, &l->scroll, e)) { + invalidate_row(l, was); + return true; + } + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + set_pressed(l, row_at(l, e->x, e->y)); + return true; + case UI_TAP: { + int i = row_at(l, e->x, e->y); + set_pressed(l, -1); + if (i >= 0) { + const phone_call_t *call = &phone_calllog()[i]; + app_dialer_call(call->name, call->phone); + } + return true; + } + default: + set_pressed(l, -1); + return false; + } +} + +void app_calllog_open(void) +{ + calllog_t *l = + ui_view_new(sizeof(*l), "Call Log", calllog_paint, calllog_event, NULL); + if (!l) + return; + l->pressed = -1; + ui_view_set_layout(&l->view, calllog_layout); + int missed = 0; + for (int i = 0; i < PHONE_CALLLOG_COUNT; i++) + missed += phone_calllog()[i].dir == PHONE_CALL_MISSED; + if (missed) + snprintf(l->missed, sizeof(l->missed), "%d missed", missed); + ui_view_show(&l->view); +} + +/* Test hook: scroll offset of the call log on top, -1 otherwise */ +int app_calllog_test_offset(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == calllog_paint ? ((calllog_t *) v)->scroll.offset + : -1; +} diff --git a/apps/cellphone/camera.c b/apps/cellphone/camera.c new file mode 100644 index 00000000..c903c50c --- /dev/null +++ b/apps/cellphone/camera.c @@ -0,0 +1,524 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Camera: a faux viewfinder over a procedural landscape. + * + * The scene is static, so nothing repaints while the camera idles: the + * shutter flash is the only animation, and it invalidates the viewfinder + * only while it fades. The scene is drawn from paths in permille + * coordinates of its rectangle, so the last-shot thumbnail is the same + * painter at a smaller size (and the selfie camera is the same painter + * mirrored). Everything is filled shapes; the coverage cache keeps + * repaints cheap. + */ + +#include + +#include "phone.h" + +#define CTRL_H 70 /* bottom control bar */ +#define VF_MARGIN 4 +#define VF_RADIUS 8 +#define SHUTTER_R 23 +#define THUMB_SZ 36 +#define FLASH_MS 220 + +#define BAR UI_RGB(0x121212) +#define MODE_ON UI_RGB(0xffd54f) +#define MODE_OFF UI_RGB(0x9e9e9e) +#define REC_RED UI_RGB(0xe53935) + +typedef struct { + ui_view_t view; + int shots; /* photos taken */ + twin_a8_t flash; /* white overlay strength, fades to 0 */ + bool video; /* video mode instead of photo */ + bool recording; + bool selfie; /* front camera: mirrored scene */ + bool shot_selfie; /* camera used for the last shot */ + int pressed; /* control under the finger, CTL_NONE when none */ +} camera_t; + +enum { CTL_NONE, CTL_SHUTTER, CTL_PHOTO, CTL_VIDEO, CTL_FLIP }; + + +static ui_rect_t viewfinder(ui_view_t *v) +{ + return ui_rect(VF_MARGIN, VF_MARGIN, PHONE_W - 2 * VF_MARGIN, + ui_view_height(v) - CTRL_H - 2 * VF_MARGIN); +} + +static ui_rect_t ctrl_bar(ui_view_t *v) +{ + return ui_rect(0, ui_view_height(v) - CTRL_H, PHONE_W, CTRL_H); +} + +/* Center line of the shutter row, and the top of the mode labels */ +static twin_coord_t ctrl_cy(ui_view_t *v) +{ + return ui_view_height(v) - CTRL_H + 44; +} + +static twin_coord_t mode_y(ui_view_t *v) +{ + return ui_view_height(v) - CTRL_H + 3; +} + +static ui_rect_t control_rect(ui_view_t *v, int ctl) +{ + switch (ctl) { + case CTL_SHUTTER: + return ui_rect(PHONE_W / 2 - SHUTTER_R - 2, ctrl_cy(v) - SHUTTER_R - 2, + 2 * SHUTTER_R + 4, 2 * SHUTTER_R + 4); + case CTL_PHOTO: + return ui_rect(PHONE_W / 2 - 64, mode_y(v), 60, 16); + case CTL_VIDEO: + return ui_rect(PHONE_W / 2 + 4, mode_y(v), 60, 16); + case CTL_FLIP: + return ui_rect(PHONE_W - 58, ctrl_cy(v) - 20, 40, 40); + default: + return ui_rect(0, 0, 0, 0); + } +} + +static ui_rect_t thumb_rect(ui_view_t *v) +{ + return ui_rect(20, ctrl_cy(v) - THUMB_SZ / 2, THUMB_SZ, THUMB_SZ); +} + +/* + * Scene + */ +typedef struct { + short x, y; /* permille of the scene rectangle */ +} pt_t; + +static const pt_t far_ridge[] = { + {0, 560}, {110, 430}, {200, 480}, {330, 330}, {450, 460}, {540, 400}, + {690, 290}, {820, 420}, {920, 370}, {1000, 410}, {1000, 760}, {0, 760}, +}; + +static const pt_t near_ridge[] = { + {0, 640}, {140, 540}, {280, 610}, {400, 560}, {560, 650}, + {740, 530}, {880, 600}, {1000, 560}, {1000, 800}, {0, 800}, +}; + +static const pt_t road[] = { + {470, 760}, + {530, 760}, + {760, 1000}, + {240, 1000}, +}; + +static const pt_t trees[] = { + {90, 790}, {150, 820}, {820, 800}, {900, 830}, {960, 790}, +}; + +typedef struct { + ui_rect_t r; + bool mirror; +} frame_t; + +static twin_fixed_t sx(const frame_t *f, int x) +{ + if (f->mirror) + x = 1000 - x; + return ui_fx(f->r.left) + ui_fx(ui_w(f->r)) / 1000 * x; +} + +static twin_fixed_t sy(const frame_t *f, int y) +{ + return ui_fx(f->r.top) + ui_fx(ui_h(f->r)) / 1000 * y; +} + +static void poly(twin_path_t *p, const frame_t *f, const pt_t *pts, int n) +{ + twin_path_move(p, sx(f, pts[0].x), sy(f, pts[0].y)); + for (int i = 1; i < n; i++) + twin_path_draw(p, sx(f, pts[i].x), sy(f, pts[i].y)); + twin_path_close(p); +} + +static void scene_poly(twin_pixmap_t *px, + const frame_t *f, + const pt_t *pts, + int n, + int y0, + twin_argb32_t c0, + int y1, + twin_argb32_t c1) +{ + twin_path_t *p = ui_path(); + poly(p, f, pts, n); + twin_paint_path_linear(px, p, 0, sy(f, y0), c0, 0, sy(f, y1), c1); + ui_path_done(p); +} + +static void scene_ellipse(twin_pixmap_t *px, + const frame_t *f, + int x, + int y, + int rx, + int ry, + twin_argb32_t c) +{ + twin_path_t *p = ui_path(); + twin_fixed_t unit = + ui_fx(ui_w(f->r) < ui_h(f->r) ? ui_w(f->r) : ui_h(f->r)); + twin_path_ellipse(p, sx(f, x), sy(f, y), unit / 1000 * rx, + unit / 1000 * ry); + twin_paint_path(px, c, p); + ui_path_done(p); +} + +#define N(a) ((int) (sizeof(a) / sizeof((a)[0]))) + +static void paint_scene(twin_pixmap_t *px, ui_rect_t r, bool mirror) +{ + frame_t f = {r, mirror}; + int h = ui_h(r); + + /* Sky: deep blue fading to a warm horizon */ + ui_vgrad(px, ui_rect(r.left, r.top, ui_w(r), h * 62 / 100), + UI_RGB(0x5a9fe0), UI_RGB(0xffdcb0)); + ui_fill(px, + ui_rect(r.left, r.top + h * 62 / 100, ui_w(r), h - h * 62 / 100), + UI_RGB(0xffdcb0)); + + /* Sun with a soft halo, and a couple of clouds */ + scene_ellipse(px, &f, 720, 270, 150, 150, ui_alpha(UI_RGB(0xfff3c4), 70)); + scene_ellipse(px, &f, 720, 270, 105, 105, ui_alpha(UI_RGB(0xfff3c4), 110)); + scene_ellipse(px, &f, 720, 270, 70, 70, UI_RGB(0xfff8e1)); + scene_ellipse(px, &f, 230, 170, 120, 26, ui_alpha(UI_WHITE, 180)); + scene_ellipse(px, &f, 300, 150, 70, 30, ui_alpha(UI_WHITE, 200)); + + scene_poly(px, &f, far_ridge, N(far_ridge), 290, UI_RGB(0x8ea2c0), 760, + UI_RGB(0x6c7f9b)); + scene_poly(px, &f, near_ridge, N(near_ridge), 530, UI_RGB(0x557a5c), 800, + UI_RGB(0x3b5a43)); + + /* Meadow, a lake, a road to the horizon, trees */ + ui_vgrad(px, + ui_rect(r.left, r.top + h * 76 / 100, ui_w(r), h - h * 76 / 100), + UI_RGB(0x7aa64f), UI_RGB(0x2f5a25)); + scene_ellipse(px, &f, 250, 860, 190, 36, UI_RGB(0x8cc4e6)); + scene_ellipse(px, &f, 230, 852, 120, 12, ui_alpha(UI_WHITE, 90)); + scene_poly(px, &f, road, N(road), 760, UI_RGB(0xa08c72), 1000, + UI_RGB(0x6d5c48)); + + twin_path_t *p = ui_path(); + for (int i = 0; i < N(trees); i++) { + pt_t tri[] = {{trees[i].x, (short) (trees[i].y - 110)}, + {(short) (trees[i].x + 30), trees[i].y}, + {(short) (trees[i].x - 30), trees[i].y}}; + poly(p, &f, tri, 3); + } + twin_paint_path(px, UI_RGB(0x1f3d1c), p); + ui_path_done(p); +} + +/* + * Round the corners of @r by painting the outside of each corner arc in + * @bg (cheaper than a mask, and the background is a flat color here). + */ +static void cut_corners(twin_pixmap_t *px, + ui_rect_t r, + int rad, + twin_argb32_t bg) +{ + twin_path_t *p = ui_path(); + struct { + twin_coord_t x, y, cx, cy; + twin_angle_t start; + } c[4] = { + {r.left, r.top, r.left + rad, r.top + rad, TWIN_ANGLE_180}, + {r.right, r.top, r.right - rad, r.top + rad, TWIN_ANGLE_270}, + {r.right, r.bottom, r.right - rad, r.bottom - rad, TWIN_ANGLE_0}, + {r.left, r.bottom, r.left + rad, r.bottom - rad, TWIN_ANGLE_90}, + }; + for (int i = 0; i < 4; i++) { + twin_path_move(p, ui_fx(c[i].x), ui_fx(c[i].y)); + twin_path_arc(p, ui_fx(c[i].cx), ui_fx(c[i].cy), ui_fx(rad), ui_fx(rad), + c[i].start, TWIN_ANGLE_90); + twin_path_close(p); + } + twin_paint_path(px, bg, p); + ui_path_done(p); +} + +static void chip(twin_pixmap_t *px, + twin_coord_t x, + twin_coord_t y, + ui_align_t align, + twin_argb32_t bg, + const char *label) +{ + twin_coord_t w = ui_text_width(FONT_SM, label) + 14; + if (align == UI_RIGHT) + x -= w; + else if (align == UI_CENTER) + x -= w / 2; + ui_rect_t r = ui_rect(x, y, w, 20); + ui_rrect(px, r, 10, bg); + ui_text_in(px, FONT_SM, r, UI_CENTER, UI_WHITE, label); +} + +/* Four L-shaped corner brackets as one path, painted once */ +static void focus_brackets(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int half, + int leg, + twin_argb32_t c) +{ + twin_path_t *p = ui_path(); + for (int i = 0; i < 4; i++) { + int sxn = i & 1 ? 1 : -1, syn = i & 2 ? 1 : -1; + twin_coord_t x = cx + sxn * half, y = cy + syn * half; + /* Horizontal and vertical legs extending inward from the corner */ + twin_coord_t hx = sxn > 0 ? x - leg : x; + twin_coord_t vy = syn > 0 ? y - leg : y; + twin_coord_t ty = syn > 0 ? y - 2 : y; + twin_coord_t tx = sxn > 0 ? x - 2 : x; + twin_path_rectangle(p, ui_fx(hx), ui_fx(ty), ui_fx(leg), ui_fx(2)); + twin_path_rectangle(p, ui_fx(tx), ui_fx(vy), ui_fx(2), ui_fx(leg)); + } + twin_paint_path(px, c, p); + ui_path_done(p); +} + +static void paint_viewfinder(camera_t *c, twin_pixmap_t *px) +{ + ui_rect_t vf = viewfinder(&c->view); + twin_coord_t cx = (vf.left + vf.right) / 2, cy = (vf.top + vf.bottom) / 2; + + paint_scene(px, vf, c->selfie); + + /* Rule-of-thirds grid */ + twin_argb32_t grid = ui_alpha(UI_WHITE, 70); + for (int i = 1; i < 3; i++) { + ui_fill(px, ui_rect(vf.left + ui_w(vf) * i / 3, vf.top, 1, ui_h(vf)), + grid); + ui_fill(px, ui_rect(vf.left, vf.top + ui_h(vf) * i / 3, ui_w(vf), 1), + grid); + } + + focus_brackets(px, cx, cy, 24, 12, ui_alpha(UI_WHITE, 220)); + ui_circle(px, cx, cy, 3, UI_RGB(0xfff176)); + ui_text(px, FONT_SM, cx, cy + 24 + 16, UI_CENTER, ui_alpha(UI_WHITE, 210), + "EV -0.3"); + + twin_argb32_t chip_bg = ui_alpha(UI_RGB(0x263238), 150); + chip(px, vf.left + 8, vf.top + 8, UI_LEFT, chip_bg, "HDR"); + chip(px, vf.right - 8, vf.top + 8, UI_RIGHT, chip_bg, + c->video ? "4K 30" : "12 MP"); + if (c->recording) + chip(px, cx, vf.top + 8, UI_CENTER, REC_RED, "REC"); + + if (c->flash) + ui_fill(px, vf, ui_alpha(UI_WHITE, c->flash)); + cut_corners(px, vf, VF_RADIUS, UI_BLACK); + ui_rrect_stroke(px, vf, VF_RADIUS, 1, UI_RGB(0x455a64)); +} + +static void paint_controls(camera_t *c, twin_pixmap_t *px, ui_rect_t clip) +{ + twin_coord_t cy = ctrl_cy(&c->view); + ui_fill(px, ctrl_bar(&c->view), BAR); + + ui_rect_t pr = control_rect(&c->view, CTL_PHOTO), + vr = control_rect(&c->view, CTL_VIDEO); + ui_text_in(px, FONT_SM, pr, UI_RIGHT, c->video ? MODE_OFF : MODE_ON, + "Photo"); + ui_text_in(px, FONT_SM, vr, UI_LEFT, c->video ? MODE_ON : MODE_OFF, + "Video"); + + /* Last shot: a miniature of the scene, with the shot count */ + ui_rect_t t = thumb_rect(&c->view); + if (ui_intersects(clip, ui_inset(t, -10))) { + if (c->shots) { + paint_scene(px, t, c->shot_selfie); + cut_corners(px, t, 6, BAR); + ui_rrect_stroke(px, t, 6, 2, UI_WHITE); + char n[12]; + snprintf(n, sizeof(n), "%d", c->shots > 99 ? 99 : c->shots); + twin_coord_t bx = t.right - 2, by = t.top + 2; + ui_circle(px, bx, by, 9, phone_theme()->primary); + ui_text_in(px, FONT_SM, ui_rect(bx - 9, by - 9, 18, 18), UI_CENTER, + UI_WHITE, n); + } else { + ui_rrect(px, t, 6, UI_RGB(0x2a2a2a)); + ui_icon(px, TWIN_SYM_IMAGE, FONT_NORMAL, (t.left + t.right) / 2, + (t.top + t.bottom) / 2, MODE_OFF); + } + } + + /* Shutter: white ring around a white (photo) or red (video) center */ + twin_coord_t cx = PHONE_W / 2; + bool down = c->pressed == CTL_SHUTTER; + ui_ring(px, cx, cy, SHUTTER_R, 3, UI_WHITE); + if (!c->video) { + ui_circle(px, cx, cy, down ? 16 : 18, + down ? UI_RGB(0xcfcfcf) : UI_WHITE); + } else if (c->recording) { + ui_rrect(px, ui_rect(cx - 9, cy - 9, 18, 18), 4, + down ? ui_darken(REC_RED, 20) : REC_RED); + } else { + ui_circle(px, cx, cy, down ? 16 : 18, + down ? ui_darken(REC_RED, 20) : REC_RED); + } + + /* Front/back camera switch */ + ui_rect_t fr = control_rect(&c->view, CTL_FLIP); + ui_circle(px, (fr.left + fr.right) / 2, cy, 18, + c->pressed == CTL_FLIP ? UI_RGB(0x444444) : UI_RGB(0x2a2a2a)); + ui_icon(px, TWIN_SYM_REFRESH, FONT_NORMAL, (fr.left + fr.right) / 2, cy, + UI_WHITE); +} + +static void camera_paint(ui_view_t *v, twin_pixmap_t *px) +{ + camera_t *c = (camera_t *) v; + ui_rect_t clip = ui_view_clip(v); + ui_rect_t vf = viewfinder(v); + + if (ui_intersects(clip, ui_inset(vf, -VF_MARGIN))) { + ui_fill(px, ui_rect(0, 0, PHONE_W, ctrl_bar(v).top), UI_BLACK); + paint_viewfinder(c, px); + } + if (ui_intersects(clip, ctrl_bar(v))) + paint_controls(c, px, clip); +} + +static bool flash_step(ui_view_t *v, twin_fixed_t value) +{ + camera_t *c = (camera_t *) v; + int a = 255 - twin_fixed_to_int(value * 255 + TWIN_FIXED_HALF); + c->flash = (twin_a8_t) (a < 0 ? 0 : a); + ui_view_invalidate(v, viewfinder(v)); + return c->flash > 0; +} + +static void shutter(camera_t *c) +{ + ui_view_t *v = &c->view; + if (c->video) { + c->recording = !c->recording; + ui_view_invalidate(v, viewfinder(v)); + ui_view_invalidate(v, control_rect(v, CTL_SHUTTER)); + return; + } + c->shots++; + c->shot_selfie = c->selfie; + ui_view_invalidate(v, ui_inset(thumb_rect(v), -10)); + /* A fresh flash restarts from full white; a running tween keeps going */ + bool running = c->flash > 0; + c->flash = 255; + ui_view_invalidate(v, viewfinder(v)); + if (!running && !ui_view_tween(v, FLASH_MS, TwinEaseOutCubic, flash_step)) + c->flash = 0; +} + +static int control_at(ui_view_t *v, twin_coord_t x, twin_coord_t y) +{ + for (int i = CTL_SHUTTER; i <= CTL_FLIP; i++) + if (ui_hit(control_rect(v, i), x, y)) + return i; + return CTL_NONE; +} + +static void set_pressed(camera_t *c, int ctl) +{ + if (c->pressed == ctl) + return; + if (c->pressed != CTL_NONE) + ui_view_invalidate(&c->view, control_rect(&c->view, c->pressed)); + c->pressed = ctl; + if (ctl != CTL_NONE) + ui_view_invalidate(&c->view, control_rect(&c->view, ctl)); +} + +static void set_mode(camera_t *c, bool video) +{ + if (c->video == video) + return; + c->video = video; + c->recording = false; + ui_view_invalidate_all(&c->view); +} + +static bool camera_event(ui_view_t *v, const ui_event_t *e) +{ + camera_t *c = (camera_t *) v; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + set_pressed(c, control_at(v, e->x, e->y)); + return true; + case UI_TAP: { + int ctl = control_at(v, e->x, e->y); + set_pressed(c, CTL_NONE); + if (ctl == CTL_SHUTTER) + shutter(c); + else if (ctl == CTL_PHOTO || ctl == CTL_VIDEO) + set_mode(c, ctl == CTL_VIDEO); + else if (ctl == CTL_FLIP) { + c->selfie = !c->selfie; + ui_view_invalidate(v, viewfinder(v)); + } + return true; + } + case UI_RELEASE: + set_pressed(c, CTL_NONE); + /* Horizontal swipes over the viewfinder switch modes */ + if (ui_hit(viewfinder(v), e->x0, e->y0) && + (e->dx < -40 || e->dx > 40) && e->dx * e->dx > e->dy * e->dy) + set_mode(c, e->dx < 0); + return true; + case UI_KEY: + if (e->ucs4 != ' ' && e->ucs4 != UI_KEY_ENTER) + return false; + shutter(c); + return true; + default: + set_pressed(c, CTL_NONE); + return false; + } +} + +void app_camera_open(void) +{ + camera_t *c = + ui_view_new(sizeof(*c), "Camera", camera_paint, camera_event, NULL); + if (!c) + return; + c->pressed = CTL_NONE; + ui_view_show(&c->view); +} + +/* + * Test hooks + */ +static camera_t *top_camera(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == camera_paint ? (camera_t *) v : NULL; +} + +/* Shots taken by the camera on top, -1 when it is not on top */ +int app_camera_test_shots(void) +{ + camera_t *c = top_camera(); + return c ? c->shots : -1; +} + +/* Current flash strength (0 when idle) */ +int app_camera_test_flash(void) +{ + camera_t *c = top_camera(); + return c ? c->flash : -1; +} diff --git a/apps/cellphone/contacts.c b/apps/cellphone/contacts.c new file mode 100644 index 00000000..a2e42cf9 --- /dev/null +++ b/apps/cellphone/contacts.c @@ -0,0 +1,836 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Contacts: A-Z list, contact card, and a "new contact" form. + * + * Each screen is its own view. The list holds no copy of the data: it walks + * phone_contact_at() while painting, so a contact added by the form shows up + * in sorted position as soon as the list is revealed. Scrolling blits the + * rows already on screen and paints only the exposed band. + */ + +#include + +#include "data.h" +#include "phone.h" + +/* + * List + */ +#define LIST_TOP PHONE_HEADER_H +#define FRESH_MS 1200 /* how long a newly added row stays highlighted */ + +typedef struct { + ui_view_t view; + ui_scroll_t scroll; + uint32_t count; /* contact count the layout was computed for */ + int pressed; /* contact index under the finger, -1 when none */ + int fresh; /* contact just added by the form, -1 when none */ + bool plus_down; +} contacts_list_t; + +static ui_rect_t plus_rect(void) +{ + /* Add sits at the start of the header, as the HIG places it */ + return ui_rect(0, 0, UI_HEADER_SLOT, PHONE_HEADER_H); +} + +/* Content y of contact @idx's row (sections included) */ +static twin_coord_t row_content_y(uint32_t idx) +{ + twin_coord_t y = 0; + char prev = 0; + for (uint32_t i = 0; i <= idx; i++) { + const phone_contact_t *c = phone_contact_at(i); + if (!c) + break; + if (c->initial != prev) { + y += PHONE_SECTION_H; + prev = c->initial; + } + if (i < idx) + y += PHONE_LIST_ROW_H; + } + return y; +} + +static ui_rect_t row_rect(contacts_list_t *l, int idx) +{ + return ui_rect(0, + LIST_TOP + row_content_y((uint32_t) idx) - l->scroll.offset, + PHONE_W, PHONE_LIST_ROW_H); +} + +static void list_layout(ui_view_t *v) +{ + contacts_list_t *l = (contacts_list_t *) v; + l->count = phone_contact_count(); + twin_coord_t h = + l->count ? row_content_y(l->count - 1) + PHONE_LIST_ROW_H : 0; + ui_scroll_setup(&l->scroll, + ui_rect(0, LIST_TOP, PHONE_W, ui_view_height(v) - LIST_TOP), + h); + if (l->pressed >= (int) l->count) + l->pressed = -1; + if (l->fresh >= (int) l->count) + l->fresh = -1; +} + +/* Contact under client y, or -1 for sections and empty space */ +static int row_at(contacts_list_t *l, twin_coord_t y) +{ + if (!ui_hit(l->scroll.area, 0, y)) + return -1; + twin_coord_t cy = y - LIST_TOP + l->scroll.offset, top = 0; + char prev = 0; + for (uint32_t i = 0; i < l->count; i++) { + const phone_contact_t *c = phone_contact_at(i); + if (c->initial != prev) { + top += PHONE_SECTION_H; + prev = c->initial; + } + if (cy < top) + return -1; + if (cy < top + PHONE_LIST_ROW_H) + return (int) i; + top += PHONE_LIST_ROW_H; + } + return -1; +} + +static void list_paint(ui_view_t *v, twin_pixmap_t *px) +{ + contacts_list_t *l = (contacts_list_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + ui_rect_t area = l->scroll.area; + + /* Data changed under us (e.g. reset): relayout and repaint it all */ + if (l->count != phone_contact_count()) { + list_layout(v); + ui_view_invalidate_all(v); + } + + if (ui_intersects(clip, area)) { + twin_rect_t saved = ui_clip_push(px, area); + ui_fill(px, area, th->bg); + twin_coord_t y = area.top - l->scroll.offset; + char prev = 0; + for (uint32_t i = 0; i < l->count && y < clip.bottom; i++) { + const phone_contact_t *c = phone_contact_at(i); + if (c->initial != prev) { + ui_rect_t r = ui_rect(0, y, PHONE_W, PHONE_SECTION_H); + char label[2] = {c->initial, 0}; + if (ui_intersects(r, clip)) + ui_section(px, r, label); + y += PHONE_SECTION_H; + prev = c->initial; + } + ui_rect_t r = ui_rect(0, y, PHONE_W, PHONE_LIST_ROW_H); + if (ui_intersects(r, clip)) { + char initial[2] = {c->name[0], 0}; + ui_list_row(px, r, (int) i == l->pressed || (int) i == l->fresh, + initial, ui_avatar_color(c->name), c->name, + c->phone, NULL); + } + y += PHONE_LIST_ROW_H; + } + ui_scroll_bar(px, &l->scroll); + twin_pixmap_restore_clip(px, saved); + } + + ui_rect_t hdr = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); + if (ui_intersects(clip, hdr)) { + ui_header(px, hdr, TWIN_SYM_PLUS, "Contacts", NULL); + if (l->plus_down) { + /* Header buttons have no fill until pressed */ + ui_rect_t p = plus_rect(); + ui_rrect(px, ui_inset(p, 4), 6, ui_alpha(th->on_bar, 0x30)); + ui_icon(px, TWIN_SYM_PLUS, FONT_HEADING, (p.left + p.right) / 2, + (p.top + p.bottom) / 2, th->on_bar); + } + } +} + +static ui_rect_t list_row(ui_view_t *v, int idx) +{ + return row_rect((contacts_list_t *) v, idx); +} + +static void list_set_pressed(contacts_list_t *l, int idx) +{ + ui_view_set_pressed(&l->view, &l->pressed, idx, list_row); +} + +static void list_set_plus(contacts_list_t *l, bool down) +{ + if (down == l->plus_down) + return; + l->plus_down = down; + ui_view_invalidate(&l->view, plus_rect()); +} + +static void detail_open(int idx); +static void form_open(void); + +static bool list_event(ui_view_t *v, const ui_event_t *e) +{ + contacts_list_t *l = (contacts_list_t *) v; + + /* A drag must not carry the pressed highlight along: window damage is + * a single rectangle that does not follow the scroll blit, so clear + * the highlight and flush it before any pixels move. */ + if ((e->kind == UI_DRAG || e->kind == UI_RELEASE) && l->pressed >= 0) { + list_set_pressed(l, -1); + twin_window_draw(v->window); + } + if (ui_scroll_event(v, &l->scroll, e)) + return true; + + switch (e->kind) { + case UI_PRESS: + list_set_plus(l, ui_hit(plus_rect(), e->x, e->y)); + list_set_pressed(l, row_at(l, e->y)); + return true; + case UI_MOVE: + list_set_plus(l, l->plus_down && ui_hit(plus_rect(), e->x, e->y)); + if (l->pressed >= 0 && row_at(l, e->y) != l->pressed) + list_set_pressed(l, -1); + return true; + case UI_TAP: { + bool plus = l->plus_down && ui_hit(plus_rect(), e->x, e->y); + int idx = l->pressed >= 0 ? row_at(l, e->y) : -1; + list_set_plus(l, false); + list_set_pressed(l, -1); + if (plus) + form_open(); + else if (idx >= 0) + detail_open(idx); + return true; + } + default: + list_set_plus(l, false); + list_set_pressed(l, -1); + return e->kind != UI_KEY; + } +} + +static twin_time_t fresh_done(ui_view_t *v) +{ + contacts_list_t *l = (contacts_list_t *) v; + if (l->fresh >= 0) { + ui_view_invalidate(v, row_rect(l, l->fresh)); + l->fresh = -1; + } + return -1; +} + +/* + * Called by the form while the list is covered: relayout for the new + * contact, bring its row into view and flash it. The list repaints in full + * when the form pops, so moving the offset needs no blit. + */ +static void list_added(contacts_list_t *l, int idx) +{ + list_layout(&l->view); + twin_coord_t top = row_content_y((uint32_t) idx); + twin_coord_t h = ui_h(l->scroll.area); + twin_coord_t off = l->scroll.offset; + if (top - PHONE_SECTION_H < off) + off = top - PHONE_SECTION_H; + else if (top + PHONE_LIST_ROW_H > off + h) + off = top + PHONE_LIST_ROW_H - h; + if (off > ui_scroll_max(&l->scroll)) + off = ui_scroll_max(&l->scroll); + l->scroll.offset = off < 0 ? 0 : off; + l->scroll.flinging = false; + l->fresh = idx; + /* Restart the highlight: an earlier timer would end this one early. + * The fling timer is the only other one, and the fling just stopped. */ + ui_view_stop_timers(&l->view); + if (!ui_view_every(&l->view, FRESH_MS, fresh_done)) + l->fresh = -1; + ui_view_invalidate_all(&l->view); +} + +void app_contacts_open(void) +{ + contacts_list_t *l = + ui_view_new(sizeof(*l), "Contacts", list_paint, list_event, NULL); + if (!l) + return; + l->pressed = l->fresh = -1; + ui_view_set_layout(&l->view, list_layout); + ui_view_show(&l->view); +} + +/* + * Detail + */ +enum { + DETAIL_CALL, + DETAIL_MESSAGE, + DETAIL_EMAIL, + DETAIL_PHONE_ROW, + DETAIL_N, +}; + +#define AVATAR_R 30 +#define AVATAR_CY 44 +#define ACTION_Y 108 +#define ACTION_W 68 +#define ACTION_H 48 +#define CARD_Y 168 +#define CARD_ROW_H 40 + +typedef struct { + ui_view_t view; + int idx; /* contact index */ + int pressed; /* DETAIL_*, -1 when none */ +} contact_detail_t; + +static ui_rect_t detail_rect(int what) +{ + if (what == DETAIL_PHONE_ROW) + return ui_rect(PHONE_PAD, CARD_Y, PHONE_W - 2 * PHONE_PAD, CARD_ROW_H); + twin_coord_t gap = (PHONE_W - 3 * ACTION_W) / 4; + return ui_rect(gap + what * (ACTION_W + gap), ACTION_Y, ACTION_W, ACTION_H); +} + +static void action_button(twin_pixmap_t *px, + ui_rect_t r, + twin_argb32_t bg, + twin_argb32_t fg, + bool pressed, + const char *sym, + const char *label) +{ + if (pressed) + bg = ui_darken(bg, 18); + ui_rrect(px, r, 10, bg); + twin_coord_t cx = (r.left + r.right) / 2; + ui_icon(px, sym, FONT_HEADING, cx, r.top + 18, fg); + ui_text(px, FONT_SM, cx, r.bottom - 8, UI_CENTER, fg, label); +} + +static void info_row(twin_pixmap_t *px, + ui_rect_t r, + const char *label, + const char *value, + twin_argb32_t value_color) +{ + const phone_theme_t *th = phone_theme(); + ui_text(px, FONT_SM, r.left + 10, r.top + 15, UI_LEFT, th->text_sec, label); + ui_text_fit(px, FONT_NORMAL, + ui_rect(r.left + 6, r.top + 16, ui_w(r) - 40, ui_h(r) - 18), + UI_LEFT, value_color, value); +} + +static void detail_paint(ui_view_t *v, twin_pixmap_t *px) +{ + contact_detail_t *d = (contact_detail_t *) v; + const phone_theme_t *th = phone_theme(); + const phone_contact_t *c = phone_contact_at((uint32_t) d->idx); + + ui_vgrad(px, ui_rect(0, 0, PHONE_W, ACTION_Y), th->card, th->bg); + ui_fill(px, ui_rect(0, ACTION_Y, PHONE_W, ui_view_height(v) - ACTION_Y), + th->bg); + if (!c) { + ui_text(px, FONT_NORMAL, PHONE_W / 2, 120, UI_CENTER, th->text_sec, + "Contact not found"); + return; + } + + ui_circle(px, PHONE_W / 2, AVATAR_CY + 2, AVATAR_R + 1, + ui_alpha(th->text, 0x28)); + ui_avatar(px, PHONE_W / 2, AVATAR_CY, AVATAR_R, c->name[0], + ui_avatar_color(c->name)); + ui_text_fit(px, FONT_HEADING, ui_rect(PHONE_PAD, 78, PHONE_W - 16, 24), + UI_CENTER, th->text, c->name); + + action_button(px, detail_rect(DETAIL_CALL), th->call_green, + UI_RGB(0xffffff), d->pressed == DETAIL_CALL, TWIN_SYM_PHONE, + "Call"); + action_button(px, detail_rect(DETAIL_MESSAGE), th->primary, th->on_primary, + d->pressed == DETAIL_MESSAGE, TWIN_SYM_MESSAGE, "Message"); + /* No mail client: drawn for completeness, but visibly inactive */ + action_button(px, detail_rect(DETAIL_EMAIL), th->card, + ui_mix(th->card, th->text_sec, 160), false, TWIN_SYM_MAIL, + "Email"); + + ui_rect_t card = + ui_rect(PHONE_PAD, CARD_Y, PHONE_W - 2 * PHONE_PAD, 2 * CARD_ROW_H); + ui_rrect(px, card, 10, th->card); + ui_rect_t phone_row = detail_rect(DETAIL_PHONE_ROW); + if (d->pressed == DETAIL_PHONE_ROW) { + ui_rect_t hl = phone_row; + hl.bottom += 6; /* square off the lower corners under row 2 */ + ui_rrect(px, hl, 10, ui_mix(th->card, th->primary, 70)); + ui_fill(px, ui_rect(card.left, phone_row.bottom, ui_w(card), 6), + th->card); + } + info_row(px, phone_row, "mobile", c->phone, th->text); + ui_icon(px, TWIN_SYM_PHONE, FONT_NORMAL, phone_row.right - 18, + (phone_row.top + phone_row.bottom) / 2, th->call_green); + ui_fill(px, + ui_rect(card.left + 10, phone_row.bottom - 1, ui_w(card) - 20, 1), + th->separator); + ui_rect_t mail_row = + ui_rect(card.left, phone_row.bottom, ui_w(card), CARD_ROW_H); + info_row(px, mail_row, "email", c->email[0] ? c->email : "-", th->text); +} + +static int detail_hit(twin_coord_t x, twin_coord_t y) +{ + for (int i = 0; i < DETAIL_N; i++) + if (i != DETAIL_EMAIL && ui_hit(detail_rect(i), x, y)) + return i; + return -1; +} + +static ui_rect_t detail_item(ui_view_t *v, int what) +{ + (void) v; + return detail_rect(what); +} + +static void detail_set_pressed(contact_detail_t *d, int what) +{ + ui_view_set_pressed(&d->view, &d->pressed, what, detail_item); +} + +static bool detail_event(ui_view_t *v, const ui_event_t *e) +{ + contact_detail_t *d = (contact_detail_t *) v; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + detail_set_pressed(d, detail_hit(e->x, e->y)); + return true; + case UI_TAP: { + int what = detail_hit(e->x, e->y); + const phone_contact_t *c = phone_contact_at((uint32_t) d->idx); + detail_set_pressed(d, -1); + if (!c) + return true; + if (what == DETAIL_CALL || what == DETAIL_PHONE_ROW) + app_dialer_call(c->name, c->phone); + else if (what == DETAIL_MESSAGE) + app_sms_open(); + return true; + } + case UI_KEY: + return false; + default: + detail_set_pressed(d, -1); + return true; + } +} + +static void detail_open(int idx) +{ + contact_detail_t *d = + ui_view_new(sizeof(*d), "Contact", detail_paint, detail_event, NULL); + if (!d) + return; + d->idx = idx; + d->pressed = -1; + ui_view_show(&d->view); +} + +/* + * New contact form + */ +#define FIELDS 3 +#define FIELD_X PHONE_PAD +#define FIELD_W (PHONE_W - 2 * PHONE_PAD) +#define FIELD_H 30 +#define FIELD_STEP 50 +#define LABEL_H 15 +#define FORM_TOP (PHONE_HEADER_H + 6) +#define ACTIONS_H 40 + +enum { + FORM_CANCEL, + FORM_SAVE, +}; + +/* Input limits match the backing buffers in data.c (32/24/40 bytes) */ +static const struct { + const char *label, *placeholder; + int max; +} fields[FIELDS] = { + {"Name", "Ashley Johnson", 31}, + {"Phone", "+1-555-0123", 23}, + {"Email", "ashley@example.com", 39}, +}; + +typedef struct { + ui_view_t view; + ui_field_t field[FIELDS]; + int focus; /* focused field, -1 when none (keyboard hidden) */ + ui_kbd_t kbd; + int pressed; /* FORM_*, -1 when none */ + const char *error; /* validation message, NULL when none */ + bool name_error; /* outline the name field */ +} contact_form_t; + +/* + * With the keyboard up only a strip between the header and the action row + * is left, so the form lifts to keep the focused field in that strip. + */ +static twin_coord_t form_lift(const contact_form_t *f) +{ + return f->focus >= 0 ? f->focus * FIELD_STEP : 0; +} + +static twin_coord_t actions_top(const contact_form_t *f) +{ + twin_coord_t h = ui_view_height(&f->view); + return f->kbd.visible ? h - UI_KBD_H - ACTIONS_H : h - ACTIONS_H - 4; +} + +/* Label plus box of field @i, in client coordinates */ +static ui_rect_t field_slot(const contact_form_t *f, int i) +{ + return ui_rect(0, FORM_TOP + i * FIELD_STEP - form_lift(f), PHONE_W, + LABEL_H + FIELD_H); +} + +static ui_rect_t field_box(const contact_form_t *f, int i) +{ + ui_rect_t s = field_slot(f, i); + return ui_rect(FIELD_X, s.top + LABEL_H, FIELD_W, FIELD_H); +} + +static ui_rect_t form_strip(const contact_form_t *f) +{ + return ui_rect(0, PHONE_HEADER_H, PHONE_W, actions_top(f) - PHONE_HEADER_H); +} + +static ui_rect_t error_rect(void) +{ + return ui_rect(0, FORM_TOP + FIELDS * FIELD_STEP - 2, PHONE_W, 18); +} + +static ui_rect_t action_rect(const contact_form_t *f, int which) +{ + twin_coord_t w = (PHONE_W - 3 * PHONE_PAD) / 2; + return ui_rect(PHONE_PAD + which * (w + PHONE_PAD), actions_top(f) + 4, w, + ACTIONS_H - 8); +} + +static ui_rect_t kbd_rect(const contact_form_t *f) +{ + return ui_kbd_rect(ui_view_height(&f->view)); +} + +static void form_paint(ui_view_t *v, twin_pixmap_t *px) +{ + contact_form_t *f = (contact_form_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + ui_rect_t strip = form_strip(f); + + if (ui_intersects(clip, strip)) { + /* Lifted fields must not spill into the header or action row */ + twin_rect_t saved = ui_clip_push(px, strip); + ui_fill(px, strip, th->bg); + for (int i = 0; i < FIELDS; i++) { + ui_rect_t s = field_slot(f, i); + if (!ui_intersects(s, clip) || !ui_intersects(s, strip)) + continue; + ui_text(px, FONT_SM, FIELD_X + 2, s.top + 11, UI_LEFT, + f->focus == i ? th->primary : th->text_sec, + fields[i].label); + ui_rect_t box = field_box(f, i); + ui_field(px, box, &f->field[i], fields[i].placeholder); + if (i == 0 && f->name_error && f->focus != 0) + ui_rrect_stroke(px, box, 6, 1, th->missed_red); + /* Length counter once the limit is near */ + if (f->focus == i && f->field[i].len > fields[i].max - 8) { + char n[8] = {(char) ('0' + f->field[i].len / 10), + (char) ('0' + f->field[i].len % 10), + '/', + (char) ('0' + fields[i].max / 10), + (char) ('0' + fields[i].max % 10), + 0}; + ui_text(px, FONT_SM, s.right - FIELD_X - 2, s.top + 11, + UI_RIGHT, th->text_sec, n); + } + } + if (f->error && !f->kbd.visible) + ui_text_in(px, FONT_SM, error_rect(), UI_CENTER, th->missed_red, + f->error); + twin_pixmap_restore_clip(px, saved); + } + + ui_rect_t actions = ui_rect(0, actions_top(f), PHONE_W, ACTIONS_H); + if (!f->kbd.visible) + actions.bottom = ui_view_height(v); + if (ui_intersects(clip, actions)) { + ui_fill(px, actions, th->bg); + ui_fill(px, ui_rect(0, actions.top, PHONE_W, 1), th->separator); + ui_button(px, action_rect(f, FORM_CANCEL), 10, UI_BTN_CARD, + f->pressed == FORM_CANCEL, FONT_NORMAL, "Cancel"); + ui_button(px, action_rect(f, FORM_SAVE), 10, UI_BTN_PRIMARY, + f->pressed == FORM_SAVE, FONT_NORMAL, "Save"); + } + + if (f->kbd.visible && ui_intersects(clip, kbd_rect(f))) + ui_kbd_draw(px, &f->kbd, kbd_rect(f)); + + ui_rect_t hdr = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); + if (ui_intersects(clip, hdr)) + ui_header(px, hdr, NULL, "New Contact", NULL); +} + +/* Keyboard mode suited to the field: digits for the phone number, and a + * capital letter at the start of each word of a name. */ +static void kbd_adapt(contact_form_t *f, bool focus_changed) +{ + const ui_field_t *fl = &f->field[f->focus]; + if (focus_changed) + f->kbd.symbols = f->focus == 1; + if (f->focus == 0 && !f->kbd.symbols) + f->kbd.shift = !fl->len || fl->text[fl->len - 1] == ' '; + else if (focus_changed) + f->kbd.shift = false; +} + +static void form_focus(contact_form_t *f, int i) +{ + if (i == f->focus) + return; + bool was_visible = f->kbd.visible; + if (f->focus >= 0) + f->field[f->focus].focused = false; + f->focus = i; + if (i >= 0) { + f->field[i].focused = true; + f->kbd.visible = true; + kbd_adapt(f, true); + } else { + f->kbd.visible = false; + f->kbd.pressed = -1; + } + if (was_visible != f->kbd.visible) { + /* Keyboard shown or hidden: the whole layout reflows */ + ui_view_invalidate_all(&f->view); + } else { + /* Focus moved: the lifted strip and the keyboard mode changed */ + ui_view_invalidate(&f->view, form_strip(f)); + ui_view_invalidate(&f->view, kbd_rect(f)); + } +} + +static void form_clear_error(contact_form_t *f) +{ + if (!f->error && !f->name_error) + return; + f->error = NULL; + f->name_error = false; + ui_view_invalidate(&f->view, error_rect()); + ui_view_invalidate(&f->view, field_box(f, 0)); +} + +static void form_type(contact_form_t *f, int key) +{ + if (f->focus < 0) + return; + ui_field_t *fl = &f->field[f->focus]; + if (key == UI_KEY_ENTER) { + form_focus(f, f->focus + 1 < FIELDS ? f->focus + 1 : -1); + return; + } + if (key != UI_KEY_BACKSPACE && fl->len >= fields[f->focus].max) + return; + if (!ui_field_key(fl, key)) + return; + form_clear_error(f); + /* The label line carries the length counter */ + ui_view_invalidate(&f->view, field_slot(f, f->focus)); + bool shift = f->kbd.shift; + kbd_adapt(f, false); + if (shift != f->kbd.shift) + ui_view_invalidate(&f->view, kbd_rect(f)); +} + +static contacts_list_t *find_list(void); + +/* Copy @s without leading/trailing spaces */ +static void trimmed(char *dst, size_t len, const char *s) +{ + while (*s == ' ') + s++; + size_t n = strlen(s); + while (n && s[n - 1] == ' ') + n--; + if (n >= len) + n = len - 1; + memcpy(dst, s, n); + dst[n] = 0; +} + +static void form_save(contact_form_t *f) +{ + char name[sizeof(f->field[0].text)], phone[sizeof(name)], + email[sizeof(name)]; + trimmed(name, sizeof(name), f->field[0].text); + trimmed(phone, sizeof(phone), f->field[1].text); + trimmed(email, sizeof(email), f->field[2].text); + + int32_t idx = name[0] ? phone_contact_add(name, phone, email) : -1; + if (idx < 0) { + /* Hide the keyboard so the message below the fields is visible */ + form_focus(f, -1); + f->name_error = !name[0]; + f->error = name[0] ? "Contact list full" : "Name required"; + ui_view_invalidate(&f->view, error_rect()); + ui_view_invalidate(&f->view, field_box(f, 0)); + return; + } + contacts_list_t *l = find_list(); + if (l) + list_added(l, idx); + ui_view_close(&f->view); +} + +static int form_action_at(contact_form_t *f, twin_coord_t x, twin_coord_t y) +{ + if (ui_hit(action_rect(f, FORM_CANCEL), x, y)) + return FORM_CANCEL; + if (ui_hit(action_rect(f, FORM_SAVE), x, y)) + return FORM_SAVE; + return -1; +} + +static int form_field_at(contact_form_t *f, twin_coord_t x, twin_coord_t y) +{ + if (!ui_hit(form_strip(f), x, y)) + return -1; + for (int i = 0; i < FIELDS; i++) + if (ui_hit(field_slot(f, i), x, y)) + return i; + return -1; +} + +static ui_rect_t form_action(ui_view_t *v, int which) +{ + return action_rect((contact_form_t *) v, which); +} + +static void form_set_pressed(contact_form_t *f, int which) +{ + ui_view_set_pressed(&f->view, &f->pressed, which, form_action); +} + +static bool form_event(ui_view_t *v, const ui_event_t *e) +{ + contact_form_t *f = (contact_form_t *) v; + ui_rect_t kr = kbd_rect(f); + + if (e->kind == UI_KEY) { + if (f->focus < 0) + return false; + form_type(f, (int) e->ucs4); + return true; + } + + /* Gestures that start on the keyboard belong to it */ + if (f->kbd.visible && ui_hit(kr, e->x0, e->y0)) { + int key = ui_kbd_event(&f->kbd, kr, e); + if (!ui_empty(f->kbd.dirty)) + ui_view_invalidate(v, f->kbd.dirty); + if (key != UI_KEY_NONE && key != UI_KEY_SHIFT && key != UI_KEY_MODE) + form_type(f, key); + return true; + } + + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + form_set_pressed(f, form_action_at(f, e->x, e->y)); + return true; + case UI_TAP: { + int action = form_action_at(f, e->x, e->y); + int field = form_field_at(f, e->x, e->y); + form_set_pressed(f, -1); + if (action == FORM_CANCEL) + ui_view_close(v); + else if (action == FORM_SAVE) + form_save(f); + else + form_focus(f, field); /* outside every field: hide keyboard */ + return true; + } + default: + form_set_pressed(f, -1); + return true; + } +} + +static void form_open(void) +{ + contact_form_t *f = + ui_view_new(sizeof(*f), "New Contact", form_paint, form_event, NULL); + if (!f) + return; + ui_kbd_init(&f->kbd); + f->focus = f->pressed = -1; + ui_view_show(&f->view); +} + +/* The list view under the form, looked up afresh (never cached) */ +static contacts_list_t *find_list(void) +{ + twin_screen_t *s = phone_screen(); + contacts_list_t *found = NULL; + for (twin_window_t *w = twin_screen_first_window(s); w; + w = twin_window_next(w)) { + ui_view_t *v = ui_view_of(w); + if (v && v->paint == list_paint) + found = (contacts_list_t *) v; + } + return found; +} + +/* + * Test hooks: inspect the contacts screen on top of the stack + */ +int app_contacts_test_offset(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != list_paint) + return -1; + return ((contacts_list_t *) v)->scroll.offset; +} + +const char *app_contacts_test_detail(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != detail_paint) + return NULL; + const phone_contact_t *c = + phone_contact_at((uint32_t) ((contact_detail_t *) v)->idx); + return c ? c->name : NULL; +} + +const char *app_contacts_test_form_error(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != form_paint) + return NULL; + contact_form_t *f = (contact_form_t *) v; + return f->error ? f->error : ""; +} + +bool app_contacts_test_keyboard(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == form_paint && ((contact_form_t *) v)->kbd.visible; +} diff --git a/apps/cellphone/data.c b/apps/cellphone/data.c new file mode 100644 index 00000000..89ac2260 --- /dev/null +++ b/apps/cellphone/data.c @@ -0,0 +1,256 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Demo data: contacts, message threads, call log, music tracks. + * Stock records are const (flash); only contacts added at run time use RAM. + */ + +#include + +#include "data.h" + +static const phone_contact_t s_contacts_stock[PHONE_CONTACT_STOCK] = { + {"Ashley Johnson", "+1-555-0101", "ashley@example.com", 'A'}, + {"Brian Smith", "+1-555-0102", "brian@example.com", 'B'}, + {"Chris Williams", "+1-555-0103", "chris@example.com", 'C'}, + {"Daniel Brown", "+1-555-0104", "daniel@example.com", 'D'}, + {"Emily Davis", "+1-555-0105", "emily@example.com", 'E'}, + {"Frank Miller", "+1-555-0106", "frank@example.com", 'F'}, + {"Grace Wilson", "+1-555-0107", "grace@example.com", 'G'}, + {"Henry Thomas", "+1-555-0108", "henry@example.com", 'H'}, + {"Isaac Moore", "+1-555-0109", "isaac@example.com", 'I'}, + {"James Taylor", "+1-555-0110", "james@example.com", 'J'}, + {"Karen Harris", "+1-555-0111", "karen@example.com", 'K'}, + {"Laura Jackson", "+1-555-0112", "laura@example.com", 'L'}, +}; + +#define CONTACT_ADDED_MAX (PHONE_CONTACT_CAPACITY - PHONE_CONTACT_STOCK) +#define CONTACT_ORDER_ADDED 0x80u /* high bit flags an added-slot index */ + +static phone_contact_t s_contacts_added[CONTACT_ADDED_MAX]; +static char s_added_names[CONTACT_ADDED_MAX][32]; +static char s_added_phones[CONTACT_ADDED_MAX][24]; +static char s_added_emails[CONTACT_ADDED_MAX][40]; + +/* High bit of an order entry flags an added-slot index, so the visible + * list stays sorted without copying stock rows into RAM. */ +static uint8_t s_contact_order[PHONE_CONTACT_CAPACITY] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +}; +_Static_assert(PHONE_CONTACT_STOCK == 12, "s_contact_order lists the stock"); +static uint32_t s_contact_count = PHONE_CONTACT_STOCK; + +static const phone_contact_t *contact_resolve(uint8_t ord) +{ + if (ord & CONTACT_ORDER_ADDED) + return &s_contacts_added[ord & 0x7Fu]; + return &s_contacts_stock[ord]; +} + +static int32_t contact_name_cmp_folded(const char *lhs, const char *rhs) +{ + uint32_t i = 0; + + while (lhs[i] != '\0' && rhs[i] != '\0') { + char a = lhs[i]; + char b = rhs[i]; + + if (a >= 'a' && a <= 'z') + a = (char) (a - 'a' + 'A'); + if (b >= 'a' && b <= 'z') + b = (char) (b - 'a' + 'A'); + + if (a != b) + return (int32_t) ((unsigned char) a - (unsigned char) b); + i++; + } + + return (int32_t) ((unsigned char) lhs[i] - (unsigned char) rhs[i]); +} + +static char contact_initial_for(const char *name) +{ + if (name == NULL || name[0] == '\0') + return '#'; + + char c = name[0]; + if (c >= 'a' && c <= 'z') + c = (char) (c - 'a' + 'A'); + if ((c < 'A' || c > 'Z') && (c < '0' || c > '9')) + return '#'; + return c; +} + +const phone_contact_t *phone_contact_at(uint32_t idx) +{ + if (idx >= s_contact_count) + return NULL; + return contact_resolve(s_contact_order[idx]); +} + +uint32_t phone_contact_count(void) +{ + return s_contact_count; +} + +int32_t phone_contact_add(const char *name, + const char *phone, + const char *email) +{ + if (!name || name[0] == '\0') + return -1; + if (s_contact_count >= PHONE_CONTACT_CAPACITY) + return -1; + if (!phone) + phone = ""; + if (!email) + email = ""; + + uint32_t buf_idx = s_contact_count - PHONE_CONTACT_STOCK; + snprintf(s_added_names[buf_idx], sizeof(s_added_names[buf_idx]), "%s", + name); + snprintf(s_added_phones[buf_idx], sizeof(s_added_phones[buf_idx]), "%s", + phone); + snprintf(s_added_emails[buf_idx], sizeof(s_added_emails[buf_idx]), "%s", + email); + + s_contacts_added[buf_idx] = (phone_contact_t) { + .name = s_added_names[buf_idx], + .phone = s_added_phones[buf_idx], + .email = s_added_emails[buf_idx], + .initial = contact_initial_for(s_added_names[buf_idx]), + }; + + uint32_t insert_at = s_contact_count; + for (uint32_t i = 0; i < s_contact_count; i++) { + const phone_contact_t *existing = contact_resolve(s_contact_order[i]); + if (contact_name_cmp_folded(s_contacts_added[buf_idx].name, + existing->name) < 0) { + insert_at = i; + break; + } + } + + for (uint32_t i = s_contact_count; i > insert_at; i--) { + s_contact_order[i] = s_contact_order[i - 1]; + } + + s_contact_order[insert_at] = (uint8_t) (CONTACT_ORDER_ADDED | buf_idx); + s_contact_count++; + return (int32_t) insert_at; +} + +void phone_data_reset(void) +{ + for (uint32_t i = 0; i < PHONE_CONTACT_STOCK; i++) + s_contact_order[i] = (uint8_t) i; + s_contact_count = PHONE_CONTACT_STOCK; +} + +/********************* + * SMS + *********************/ +static const phone_sms_msg_t s_sms_ashley[] = { + {"Hey, you coming to the meeting?", 0, "09:15"}, + {"Yeah, on my way!", 1, "09:17"}, + {"Cool, see you in 5", 0, "09:18"}, + {"Just got here", 1, "09:22"}, + {"Conference room B", 0, "09:23"}, + {"Got it, thanks!", 1, "09:24"}, +}; + +static const phone_sms_msg_t s_sms_brian[] = { + {"Did you see the new design specs?", 0, "14:05"}, + {"Not yet, got a link?", 1, "14:10"}, + {"Sent it to your inbox", 0, "14:11"}, + {"Thanks, looks great!", 1, "14:30"}, + {"Want to chat about it tomorrow?", 0, "14:31"}, + {"Sure, mornings work for me", 1, "14:35"}, + {"10 AM then", 0, "14:36"}, +}; + +static const phone_sms_msg_t s_sms_chris[] = { + {"Happy birthday!", 1, "08:00"}, + {"Thanks so much!", 0, "08:45"}, + {"Hope you have a great one", 1, "08:46"}, + {"We should grab dinner soon", 0, "08:50"}, + {"Sounds good, how about Friday?", 1, "08:52"}, +}; + +static const phone_sms_msg_t s_sms_daniel[] = { + {"Can you pick up groceries?", 0, "17:30"}, + {"Sure, what do we need?", 1, "17:35"}, + {"Milk, eggs, bread, and coffee", 0, "17:36"}, + {"Got it. Anything else?", 1, "17:38"}, + {"That's all, thanks!", 0, "17:39"}, + {"No problem", 1, "17:40"}, + {"Actually, grab some fruit too", 0, "17:42"}, + {"Apples and bananas?", 1, "17:43"}, +}; + +static const phone_sms_thread_t s_sms_threads[PHONE_SMS_THREADS] = { + {"Ashley Johnson", "Got it, thanks!", "Today", s_sms_ashley, + sizeof(s_sms_ashley) / sizeof(s_sms_ashley[0])}, + {"Brian Smith", "10 AM then", "Today", s_sms_brian, + sizeof(s_sms_brian) / sizeof(s_sms_brian[0])}, + {"Chris Williams", "Sounds good, how about Friday?", "Yesterday", + s_sms_chris, sizeof(s_sms_chris) / sizeof(s_sms_chris[0])}, + {"Daniel Brown", "Apples and bananas?", "Yesterday", s_sms_daniel, + sizeof(s_sms_daniel) / sizeof(s_sms_daniel[0])}, +}; + +const phone_sms_thread_t *phone_sms_threads(void) +{ + return s_sms_threads; +} + +/********************* + * CALL LOG + *********************/ +static const phone_call_t s_calllog[PHONE_CALLLOG_COUNT] = { + {"Ashley Johnson", "+1-555-0101", "Today, 14:22", PHONE_CALL_IN}, + {"Brian Smith", "+1-555-0102", "Today, 13:05", PHONE_CALL_OUT}, + {"Unknown", "+1-555-9999", "Today, 11:30", PHONE_CALL_MISSED}, + {"Chris Williams", "+1-555-0103", "Today, 10:15", PHONE_CALL_IN}, + {"Daniel Brown", "+1-555-0104", "Today, 09:45", PHONE_CALL_OUT}, + {"Emily Davis", "+1-555-0105", "Yesterday, 18:30", PHONE_CALL_MISSED}, + {"Frank Miller", "+1-555-0106", "Yesterday, 16:20", PHONE_CALL_IN}, + {"Grace Wilson", "+1-555-0107", "Yesterday, 14:10", PHONE_CALL_OUT}, + {"Ashley Johnson", "+1-555-0101", "Yesterday, 12:00", PHONE_CALL_IN}, + {"Henry Thomas", "+1-555-0108", "Yesterday, 10:30", PHONE_CALL_MISSED}, + {"Isaac Moore", "+1-555-0109", "Apr 23, 17:45", PHONE_CALL_OUT}, + {"James Taylor", "+1-555-0110", "Apr 23, 15:00", PHONE_CALL_IN}, + {"Karen Harris", "+1-555-0111", "Apr 22, 09:30", PHONE_CALL_MISSED}, + {"Laura Jackson", "+1-555-0112", "Apr 22, 08:15", PHONE_CALL_OUT}, + {"Brian Smith", "+1-555-0102", "Apr 21, 20:00", PHONE_CALL_IN}, +}; + +const phone_call_t *phone_calllog(void) +{ + return s_calllog; +} + +/********************* + * MUSIC + *********************/ +static const phone_track_t s_tracks[PHONE_TRACK_COUNT] = { + {"Summer in the City", "The Highway Drifters", "Open Road", 214}, + {"Heart on the Run", "Ocean Avenue", "Coastline", 187}, + {"Tomorrow's Light", "Neon Skyline", "City Lights", 245}, + {"Coming Home", "The Highway Drifters", "Open Road", 198}, + {"Out of the Blue", "Ocean Avenue", "Coastline", 220}, + {"Last Train Out", "Neon Skyline", "City Lights", 176}, + {"Catch the Wind", "Neon Skyline", "City Lights", 203}, + {"Roadside Diner", "The Highway Drifters", "Open Road", 262}, + {"Letters Unsent", "Ocean Avenue", "Coastline", 195}, + {"Goodbye for Now", "Neon Skyline", "City Lights", 240}, +}; + +const phone_track_t *phone_tracks(void) +{ + return s_tracks; +} diff --git a/apps/cellphone/data.h b/apps/cellphone/data.h new file mode 100644 index 00000000..00e07d7e --- /dev/null +++ b/apps/cellphone/data.h @@ -0,0 +1,76 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#ifndef _APPS_PHONE_DATA_H_ +#define _APPS_PHONE_DATA_H_ + +#include +#include + +typedef struct { + const char *name; + const char *phone; + const char *email; + char initial; /* first letter, for section headers */ +} phone_contact_t; + +#define PHONE_CONTACT_STOCK 12 +#define PHONE_CONTACT_CAPACITY 16 + +/* Contacts sorted by name; index 0 .. phone_contact_count() - 1 */ +const phone_contact_t *phone_contact_at(uint32_t idx); +uint32_t phone_contact_count(void); +/* Insert in sorted order; returns the new index or -1 when full/invalid */ +int32_t phone_contact_add(const char *name, + const char *phone, + const char *email); +/* Drop contacts added at run time */ +void phone_data_reset(void); + +typedef struct { + const char *text; + uint8_t is_sent; /* 1 = sent by user, 0 = received */ + const char *time; +} phone_sms_msg_t; + +typedef struct { + const char *contact_name; + const char *preview; + const char *timestamp; + const phone_sms_msg_t *messages; + uint32_t msg_count; +} phone_sms_thread_t; + +#define PHONE_SMS_THREADS 4 +const phone_sms_thread_t *phone_sms_threads(void); + +typedef enum { + PHONE_CALL_IN = 0, + PHONE_CALL_OUT, + PHONE_CALL_MISSED, +} phone_call_dir_t; + +typedef struct { + const char *name; + const char *phone; + const char *time; + phone_call_dir_t dir; +} phone_call_t; + +#define PHONE_CALLLOG_COUNT 15 +const phone_call_t *phone_calllog(void); + +typedef struct { + const char *title; + const char *artist; + const char *album; + uint32_t duration_sec; +} phone_track_t; + +#define PHONE_TRACK_COUNT 10 +const phone_track_t *phone_tracks(void); + +#endif /* _APPS_PHONE_DATA_H_ */ diff --git a/apps/cellphone/dialer.c b/apps/cellphone/dialer.c new file mode 100644 index 00000000..bb73bb07 --- /dev/null +++ b/apps/cellphone/dialer.c @@ -0,0 +1,527 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Phone: dialer keypad and the in-call screen. + * + * The dialer and the call are separate views so the call can be placed + * from Contacts or the Call Log without a keypad underneath. Both repaint + * only what a press changes: one key, the number display, or the timer. + */ + +#include +#include + +#include "data.h" +#include "phone.h" + +#define NUM_MAX 20 +#define DISPLAY_H 50 +#define BOTTOM_H 48 +#define KEY_ROWS 4 +#define KEY_COLS 3 +#define KEY_W (PHONE_W / KEY_COLS) +#define KEY_GAP 3 + +/* Pseudo keys past the 12 keypad keys */ +enum { KEY_DEL = 12, KEY_CALL, N_HIT }; + +typedef struct { + char digit; + const char *letters; +} dial_key_t; + +static const dial_key_t keys[12] = { + {'1', ""}, {'2', "ABC"}, {'3', "DEF"}, {'4', "GHI"}, + {'5', "JKL"}, {'6', "MNO"}, {'7', "PQRS"}, {'8', "TUV"}, + {'9', "WXYZ"}, {'*', ""}, {'0', "+"}, {'#', ""}, +}; + +typedef struct { + ui_view_t view; + char number[NUM_MAX + 1]; + int len; + int pressed; /* key or KEY_DEL/KEY_CALL, -1 when none */ +} dialer_t; + +/* + * Shared helpers + */ + +/* Compare phone numbers by their digits only ("15550101" = "+1-555-0101") */ +static bool same_number(const char *a, const char *b) +{ + for (;;) { + while (*a && (*a < '0' || *a > '9')) + a++; + while (*b && (*b < '0' || *b > '9')) + b++; + if (!*a || !*b) + return !*a && !*b; + if (*a++ != *b++) + return false; + } +} + +static const char *contact_for(const char *number) +{ + if (!number || !*number) + return NULL; + for (uint32_t i = 0; i < phone_contact_count(); i++) { + const phone_contact_t *c = phone_contact_at(i); + if (c && c->phone && same_number(c->phone, number)) + return c->name; + } + return NULL; +} + +/* + * Draw a phone number centered in @r, shrinking once and then dropping + * leading digits: the digits just typed are the ones worth seeing. + */ +static void draw_number(twin_pixmap_t *px, + ui_rect_t r, + twin_coord_t baseline, + twin_argb32_t color, + const char *s) +{ + twin_coord_t room = ui_w(r) - 8, cx = (r.left + r.right) / 2; + int size = FONT_LARGE; + if (ui_text_width(size, s) > room) + size = FONT_HEADING; + if (ui_text_width(size, s) <= room) { + ui_text(px, size, cx, baseline, UI_CENTER, color, s); + return; + } + char buf[NUM_MAX + 8]; + size_t n = strlen(s); + const char *tail = s; + do { + tail++; + n--; + snprintf(buf, sizeof(buf), "...%s", tail); + } while (n > 1 && ui_text_width(size, buf) > room); + ui_text(px, size, cx, baseline, UI_CENTER, color, buf); +} + +/* + * In-call screen + */ +enum { BTN_MUTE, BTN_SPEAKER, BTN_END, N_BTN }; + +#define AVATAR_CY 64 +#define AVATAR_R 30 +#define TIMER_TOP 148 +#define ROW_CY 204 +#define TOGGLE_R 22 +#define END_R 26 + +typedef struct { + ui_view_t view; + char name[40]; /* empty when the number matches no contact */ + char number[32]; + int seconds; + bool muted, speaker; + int pressed; /* BTN_* or -1 */ +} incall_t; + +static ui_rect_t timer_rect(void) +{ + return ui_rect(PHONE_W / 2 - 40, TIMER_TOP, 80, 22); +} + +static twin_coord_t button_cx(int b) +{ + return b == BTN_MUTE ? 48 : b == BTN_SPEAKER ? PHONE_W - 48 : PHONE_W / 2; +} + +/* Hit and repaint area of a bottom-row button, caption included */ +static ui_rect_t button_rect(int b) +{ + return ui_rect(button_cx(b) - 36, ROW_CY - END_R - 2, 72, 2 * END_R + 26); +} + +static void paint_button(twin_pixmap_t *px, incall_t *c, int b) +{ + const phone_theme_t *th = phone_theme(); + twin_coord_t cx = button_cx(b); + bool on = b == BTN_MUTE ? c->muted : b == BTN_SPEAKER && c->speaker; + twin_argb32_t bg, fg, cap = on ? th->primary : th->text_sec; + const char *sym, *label; + int r = TOGGLE_R; + + if (b == BTN_END) { + bg = th->call_red; + fg = UI_RGB(0xffffff); + sym = TWIN_SYM_HANGUP; + label = "End"; + r = END_R; + } else { + bg = on ? th->primary : ui_mix(th->card, th->indicator, 110); + fg = on ? th->on_primary : th->text; + sym = b == BTN_MUTE ? TWIN_SYM_MIC : TWIN_SYM_VOLUME; + label = b == BTN_MUTE ? "Mute" : "Speaker"; + } + if (c->pressed == b) + bg = ui_darken(bg, 18); + ui_circle(px, cx, ROW_CY, r, bg); + ui_icon(px, sym, b == BTN_END ? FONT_LARGE : FONT_HEADING, cx, ROW_CY, fg); + /* No slashed-mic glyph in the icon font: strike it through */ + if (b == BTN_MUTE && on) + ui_line(px, cx - 9, ROW_CY - 9, cx + 9, ROW_CY + 9, 2, fg); + ui_text(px, FONT_SM, cx, ROW_CY + END_R + 16, UI_CENTER, cap, label); +} + +static void incall_paint(ui_view_t *v, twin_pixmap_t *px) +{ + incall_t *c = (incall_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + twin_coord_t cx = PHONE_W / 2; + + ui_vgrad(px, ui_view_bounds(v), th->card, th->bg); + + if (ui_intersects(clip, ui_rect(0, 0, PHONE_W, TIMER_TOP))) { + ui_text(px, FONT_SM, cx, 18, UI_CENTER, th->text_sec, "Calling..."); + ui_ring(px, cx, AVATAR_CY, AVATAR_R + 5, 2, + ui_alpha(th->call_green, 0x90)); + if (c->name[0]) { + ui_avatar(px, cx, AVATAR_CY, AVATAR_R, c->name[0], + ui_avatar_color(c->name)); + } else { + ui_circle(px, cx, AVATAR_CY, AVATAR_R, th->indicator); + ui_icon(px, TWIN_SYM_USER, FONT_LARGE, cx, AVATAR_CY, + th->on_primary); + } + ui_text_fit(px, FONT_LARGE, ui_rect(8, 104, PHONE_W - 16, 26), + UI_CENTER, th->text, c->name[0] ? c->name : "Unknown"); + ui_text_fit(px, FONT_NORMAL, ui_rect(8, 128, PHONE_W - 16, 18), + UI_CENTER, th->text_sec, c->number); + } + if (ui_intersects(clip, timer_rect())) { + char t[8]; + snprintf(t, sizeof(t), "%02d:%02d", (c->seconds / 60) % 100, + c->seconds % 60); + ui_text_in(px, FONT_HEADING, timer_rect(), UI_CENTER, th->primary, t); + } + for (int b = 0; b < N_BTN; b++) + if (ui_intersects(clip, button_rect(b))) + paint_button(px, c, b); +} + +static twin_time_t incall_tick(ui_view_t *v) +{ + incall_t *c = (incall_t *) v; + c->seconds++; + ui_view_invalidate(v, timer_rect()); + return 1000; +} + +static int incall_button_at(twin_coord_t x, twin_coord_t y) +{ + for (int b = 0; b < N_BTN; b++) + if (ui_hit(button_rect(b), x, y)) + return b; + return -1; +} + +static ui_rect_t incall_button(ui_view_t *v, int b) +{ + (void) v; + return button_rect(b); +} + +static void incall_set_pressed(incall_t *c, int b) +{ + ui_view_set_pressed(&c->view, &c->pressed, b, incall_button); +} + +static bool incall_event(ui_view_t *v, const ui_event_t *e) +{ + incall_t *c = (incall_t *) v; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + incall_set_pressed(c, incall_button_at(e->x, e->y)); + return true; + case UI_TAP: { + int b = incall_button_at(e->x, e->y); + incall_set_pressed(c, -1); + if (b == BTN_END) { + ui_view_close(v); /* frees c */ + } else if (b >= 0) { + if (b == BTN_MUTE) + c->muted = !c->muted; + else + c->speaker = !c->speaker; + ui_view_invalidate(v, button_rect(b)); + } + return true; + } + default: + incall_set_pressed(c, -1); + return false; + } +} + +void app_dialer_call(const char *name, const char *number) +{ + if (!number || !*number) + return; + incall_t *c = + ui_view_new(sizeof(*c), "Call", incall_paint, incall_event, NULL); + if (!c) + return; + /* Copies: the caller's strings may live in a view that closes */ + if (!name || !*name || !strcmp(name, "Unknown")) + name = contact_for(number); + snprintf(c->name, sizeof(c->name), "%s", name ? name : ""); + snprintf(c->number, sizeof(c->number), "%s", number); + c->pressed = -1; + ui_view_every(&c->view, 1000, incall_tick); + ui_view_show(&c->view); +} + +/* + * Dialer + */ +static ui_rect_t display_rect(void) +{ + return ui_rect(0, 0, PHONE_W, DISPLAY_H); +} + +static ui_rect_t key_rect(ui_view_t *v, int i) +{ + twin_coord_t h = ui_view_height(v), bottom_y = h - BOTTOM_H; + twin_coord_t key_h = (h - DISPLAY_H - BOTTOM_H) / KEY_ROWS; + if (i == KEY_DEL) + return ui_rect(KEY_GAP * 2, bottom_y + 5, 70, BOTTOM_H - 10); + if (i == KEY_CALL) + return ui_rect(KEY_GAP * 2 + 76, bottom_y + 5, + PHONE_W - KEY_GAP * 4 - 76, BOTTOM_H - 10); + return ui_rect((i % KEY_COLS) * KEY_W + KEY_GAP, + DISPLAY_H + (i / KEY_COLS) * key_h + KEY_GAP, + KEY_W - 2 * KEY_GAP, key_h - 2 * KEY_GAP); +} + +static void paint_key(twin_pixmap_t *px, dialer_t *d, int i) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = key_rect(&d->view, i); + bool down = d->pressed == i; + twin_coord_t cx = (r.left + r.right) / 2, cy = (r.top + r.bottom) / 2; + + if (i == KEY_DEL) { + twin_argb32_t bg = ui_mix(th->card, th->indicator, 110); + ui_rrect(px, r, 10, down ? th->primary_dk : bg); + ui_icon(px, TWIN_SYM_BACKSPACE, FONT_HEADING, cx, cy, + down ? th->on_primary + : d->len ? th->text + : th->text_sec); + return; + } + if (i == KEY_CALL) { + /* Dimmed while there is nothing to dial */ + twin_argb32_t bg = + d->len ? th->call_green : ui_mix(th->call_green, th->card, 110); + ui_rrect(px, r, ui_h(r) / 2, down ? ui_darken(bg, 18) : bg); + twin_coord_t w = 24 + ui_text_width(FONT_HEADING, "Call"); + twin_coord_t x = cx - w / 2; + ui_icon(px, TWIN_SYM_PHONE, FONT_HEADING, x + 8, cy, UI_RGB(0xffffff)); + ui_text(px, FONT_HEADING, x + 24, + ui_baseline(FONT_HEADING, r.top, ui_h(r)), UI_LEFT, + UI_RGB(0xffffff), "Call"); + return; + } + char s[2] = {keys[i].digit, 0}; + twin_argb32_t bg = + down ? th->primary_dk : ui_mix(th->card, th->indicator, 110); + ui_rrect(px, r, 10, bg); + if (!keys[i].letters[0]) { + /* * and # sit lower in the em box; center them optically */ + ui_text_in(px, FONT_LARGE, + ui_rect(r.left, r.top + (keys[i].digit == '*' ? 5 : 0), + ui_w(r), ui_h(r)), + UI_CENTER, down ? th->on_primary : th->text, s); + return; + } + ui_text(px, FONT_LARGE, cx, r.top + 21, UI_CENTER, + down ? th->on_primary : th->text, s); + ui_text(px, 9, cx, r.top + 31, UI_CENTER, + down ? th->on_primary : th->text_sec, keys[i].letters); +} + +static void dialer_paint(ui_view_t *v, twin_pixmap_t *px) +{ + dialer_t *d = (dialer_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)); + if (!d->len) { + ui_text_in(px, FONT_NORMAL, display_rect(), UI_CENTER, th->text_sec, + "Enter a number"); + } else { + const char *who = contact_for(d->number); + draw_number(px, display_rect(), who ? 26 : 33, th->text, d->number); + if (who) + ui_text_fit(px, FONT_SM, ui_rect(8, 31, PHONE_W - 16, 16), + UI_CENTER, th->primary, who); + } + ui_separator(px, display_rect()); + } + ui_rect_t pad = + ui_rect(0, DISPLAY_H, PHONE_W, ui_view_height(v) - DISPLAY_H); + if (ui_intersects(clip, pad)) + ui_fill(px, pad, th->card); + for (int i = 0; i < N_HIT; i++) + if (ui_intersects(clip, key_rect(v, i))) + paint_key(px, d, i); +} + +static int dialer_key_at(ui_view_t *v, twin_coord_t x, twin_coord_t y) +{ + for (int i = 0; i < N_HIT; i++) + if (ui_hit(key_rect(v, i), x, y)) + return i; + return -1; +} + +static void dialer_set_pressed(dialer_t *d, int k) +{ + ui_view_set_pressed(&d->view, &d->pressed, k, key_rect); +} + +static void dialer_changed(dialer_t *d, bool was_empty) +{ + ui_view_invalidate(&d->view, display_rect()); + /* Backspace and Call change look when the number empties or fills */ + if (was_empty != !d->len) { + ui_view_invalidate(&d->view, key_rect(&d->view, KEY_DEL)); + ui_view_invalidate(&d->view, key_rect(&d->view, KEY_CALL)); + } +} + +static void dialer_press(dialer_t *d, int k) +{ + bool was_empty = !d->len; + if (k == KEY_CALL) { + if (d->len) + app_dialer_call(NULL, d->number); + return; + } + if (k == KEY_DEL) { + if (!d->len) + return; + d->number[--d->len] = 0; + } else if (k >= 0 && k < 12) { + if (d->len >= NUM_MAX) + return; + d->number[d->len++] = keys[k].digit; + d->number[d->len] = 0; + } else { + return; + } + dialer_changed(d, was_empty); +} + +/* Map a typed character to a keypad key; '+' types 0's letter */ +static int dialer_key_for(int ch) +{ + if (ch == UI_KEY_BACKSPACE) + return KEY_DEL; + if (ch == UI_KEY_ENTER) + return KEY_CALL; + if (ch < 0) + return -1; + for (int i = 0; i < 12; i++) + if (keys[i].digit == (char) ch) + return i; + return -1; +} + +static bool dialer_event(ui_view_t *v, const ui_event_t *e) +{ + dialer_t *d = (dialer_t *) v; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + dialer_set_pressed(d, dialer_key_at(v, e->x, e->y)); + return true; + case UI_TAP: { + int k = dialer_key_at(v, e->x, e->y); + dialer_set_pressed(d, -1); + dialer_press(d, k); + return true; + } + case UI_KEY: { + if (e->ucs4 == '+') { + if (d->len < NUM_MAX) { + bool was_empty = !d->len; + d->number[d->len++] = '+'; + d->number[d->len] = 0; + dialer_changed(d, was_empty); + } + return true; + } + int k = dialer_key_for(ui_key_ascii(e)); + if (k < 0) + return false; + dialer_press(d, k); + return true; + } + default: + dialer_set_pressed(d, -1); + return false; + } +} + +void app_dialer_open(void) +{ + dialer_t *d = + ui_view_new(sizeof(*d), "Phone", dialer_paint, dialer_event, NULL); + if (!d) + return; + d->pressed = -1; + ui_view_show(&d->view); +} + +/* + * Test hooks: inspect the dialer or call on top of the stack + */ +const char *app_dialer_test_number(void) +{ + ui_view_t *v = phone_top(); + if (v && v->paint == dialer_paint) + return ((dialer_t *) v)->number; + if (v && v->paint == incall_paint) + return ((incall_t *) v)->number; + return NULL; +} + +const char *app_dialer_test_name(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == incall_paint ? ((incall_t *) v)->name : NULL; +} + +/* Elapsed call seconds, or -1 when no call is on top */ +int app_dialer_test_seconds(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == incall_paint ? ((incall_t *) v)->seconds : -1; +} + +/* Bit 0: muted, bit 1: speaker */ +int app_dialer_test_toggles(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != incall_paint) + return -1; + incall_t *c = (incall_t *) v; + return (c->muted ? 1 : 0) | (c->speaker ? 2 : 0); +} diff --git a/apps/cellphone/games-internal.h b/apps/cellphone/games-internal.h new file mode 100644 index 00000000..917b9ccc --- /dev/null +++ b/apps/cellphone/games-internal.h @@ -0,0 +1,146 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Shared by the arcade's files: games.c (helpers and the view) and one file + * per game (snake.c, pong.c, tetris.c). + */ + +#ifndef _GAMES_INTERNAL_H_ +#define _GAMES_INTERNAL_H_ + +#include "phone.h" + +/* Layout, client coordinates */ +#define HEADER_H PHONE_HEADER_H +#define PANEL_X 8 +#define PANEL_Y 36 +#define PANEL_W (PHONE_W - 2 * PANEL_X) +#define PANEL_H 200 +#define PANEL_R 14 +#define INNER_X (PANEL_X + 8) +#define INNER_Y (PANEL_Y + 8) + +/* Board colors: a console look that stays dark under every theme */ +#define C_PANEL UI_RGB(0x101614) +#define C_BORDER UI_RGB(0x5c746b) +#define C_CELL_A UI_RGB(0x141c19) +#define C_CELL_B UI_RGB(0x18221e) +#define C_DIM UI_RGB(0x34524a) +#define C_LABEL UI_RGB(0x8fa59c) + +/* Snake: 16 x 14 cells of 13 px */ +#define SN_COLS 16 +#define SN_ROWS 14 +#define SN_CELL 13 +#define SN_N (SN_COLS * SN_ROWS) +#define SN_START 4 + +/* Pong: integer subpixels, 1/256 px */ +#define PG_S 256 +#define PG_BALL 8 +#define PG_PW 6 +#define PG_PH 34 +#define PG_INSET 6 +#define PG_V0 (4 * PG_S) /* serve speed per tick */ +#define PG_VMAX (9 * PG_S) /* speed cap */ +#define PG_PSPEED (3 * PG_S) /* paddle speed per tick */ +#define PG_SERVE_TICKS 12 + +/* Tetris: 10 x 16 cells of 11 px */ +#define TT_COLS 10 +#define TT_ROWS 16 +#define TT_CELL 11 +#define TT_X (INNER_X + 10) +#define TT_Y (INNER_Y + 4) +#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 */ + int head, len; + int dx, dy; + int fx, fy; /* fruit */ +} snake_t; + +typedef struct { + int32_t bx, by, vx, vy; /* ball top-left and velocity, subpixels */ + int32_t py[2]; /* paddle tops, subpixels: 0 left, 1 right */ + int aim[2]; /* per-approach aiming error, px */ + int serve; /* ticks until the ball launches, 0 in play */ + bool serve_right; +} pong_t; + +typedef struct { + uint8_t field[TT_ROWS][TT_COLS]; /* 0 empty, else color index + 1 */ + int piece, rot, x, y; + int next; + int target_x, target_rot; +} tetris_t; + +typedef struct { + ui_view_t view; + game_kind_t kind; + uint32_t rng; + bool paused; + int score, best; + int steps; /* ticks played */ + int events; /* meals, paddle hits, pieces placed */ + int full; /* whole-board repaints (restarts, line clears) */ + ui_rect_t pend; /* damage batched by mark() */ + union { + snake_t snake; + pong_t pong; + tetris_t tetris; + } u; +} game_t; + +typedef struct { + const char *name; + twin_time_t period; /* ms per step */ + /* Cell grid for mark_cell(); zero for games drawn in pixels */ + int cols, rows, cell; + twin_coord_t ox, oy; + 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) */ +uint32_t game_rnd(game_t *g); +int game_rnd_below(game_t *g, int n); +ui_rect_t game_panel_rect(void); +ui_rect_t game_inner_rect(void); +/* Damage @r, batching it with nearby rectangles */ +void game_mark(game_t *g, ui_rect_t r); +ui_rect_t game_cell_rect(const game_t *g, int c, int r); +/* Cells-changed hook shared by the grid games */ +void game_mark_cell(game_t *g, int c, int r); +/* Range of cells of the current grid that intersect @clip */ +void game_cell_span(const game_t *g, + ui_rect_t clip, + int *c0, + int *c1, + int *r0, + int *r1); +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_ */ diff --git a/apps/cellphone/games.c b/apps/cellphone/games.c new file mode 100644 index 00000000..d4dcc604 --- /dev/null +++ b/apps/cellphone/games.c @@ -0,0 +1,322 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Arcade: Snake, Pong and Tetris playing themselves. + * + * 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. + * + * 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 + * rectangles and paints a batch right away when the next one is far from + * it, keeping the composited area close to the sum of the changed cells. + * + * Each open seeds its own PRNG with a fixed value, so a game replays the + * same way every time; tests rely on that. + */ + +#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, +}; + +/* + * Helpers + */ + +uint32_t game_rnd(game_t *g) +{ + /* xorshift32: tiny, deterministic, good enough for game choices */ + uint32_t x = g->rng; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return g->rng = x; +} + +int game_rnd_below(game_t *g, int n) +{ + return (int) (game_rnd(g) % (uint32_t) n); +} + +ui_rect_t game_panel_rect(void) +{ + return ui_rect(PANEL_X, PANEL_Y, PANEL_W, PANEL_H); +} + +ui_rect_t game_inner_rect(void) +{ + return ui_inset(game_panel_rect(), 8); +} + +static ui_rect_t score_rect(void) +{ + return ui_rect(PHONE_W / 2 - 24, 0, PHONE_W / 2 + 24, HEADER_H - 1); +} + +static ui_rect_t overlay_rect(void) +{ + ui_rect_t p = game_panel_rect(); + return ui_rect((p.left + p.right) / 2 - 64, (p.top + p.bottom) / 2 - 28, + 128, 56); +} + +static int area(ui_rect_t r) +{ + return ui_w(r) * ui_h(r); +} + + +static void flush(game_t *g, bool now) +{ + if (ui_empty(g->pend)) + return; + ui_view_invalidate(&g->view, g->pend); + g->pend = (ui_rect_t) {0, 0, 0, 0}; + /* Paint the batch before the next far-away one widens the window's + * single damage box; the screen keeps a list, so both stay small. */ + if (now) + twin_window_draw(g->view.window); +} + +/* Damage @r, batching it with nearby rectangles */ +void game_mark(game_t *g, ui_rect_t r) +{ + if (ui_empty(r)) + return; + if (ui_empty(g->pend)) { + g->pend = r; + return; + } + ui_rect_t u = ui_rect_union(g->pend, r); + if (area(u) * 2 <= (area(g->pend) + area(r)) * 3) { + g->pend = u; + return; + } + flush(g, true); + g->pend = r; +} + +ui_rect_t game_cell_rect(const game_t *g, int c, int r) +{ + const game_ops_t *o = ops[g->kind]; + 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]; + if (c >= 0 && r >= 0 && c < o->cols && r < o->rows) + game_mark(g, game_cell_rect(g, c, r)); +} + +/* Range of cells of the current grid that intersect @clip */ +void game_cell_span(const game_t *g, + ui_rect_t clip, + int *c0, + int *c1, + int *r0, + int *r1) +{ + const game_ops_t *o = ops[g->kind]; + 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; + d = d < 0 ? -1 : d / o->cell; + *c0 = a < 0 ? 0 : a; + *r0 = c < 0 ? 0 : c; + *c1 = b >= o->cols ? o->cols - 1 : b; + *r1 = d >= o->rows ? o->rows - 1 : d; +} + +void game_set_score(game_t *g, int score) +{ + if (score == g->score) + return; + g->score = score; + if (score > g->best) + g->best = score; + game_mark(g, score_rect()); +} + +/* Start a new round and repaint the board */ +void game_restart(game_t *g) +{ + game_set_score(g, 0); + ops[g->kind]->init(g); + g->full++; + game_mark(g, game_inner_rect()); +} + +/* + * View + */ + +static void game_paint(ui_view_t *v, twin_pixmap_t *px) +{ + game_t *g = (game_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + ui_rect_t panel = game_panel_rect(), in = game_inner_rect(); + + if (clip.top < HEADER_H) { + char buf[40]; + 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); + } + /* Ticks damage cells inside the panel: fill just the clip there */ + if (ui_rect_contains(in, clip)) { + ui_fill(px, clip, C_PANEL); + } else { + ui_fill(px, ui_rect(0, HEADER_H, PHONE_W, ui_view_height(v) - HEADER_H), + 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"); + } + ops[g->kind]->paint(g, px, clip); + + ui_rect_t o = overlay_rect(); + if (g->paused && ui_intersects(clip, o)) { + ui_rrect(px, o, 12, ui_alpha(UI_RGB(0x000000), 0xc0)); + ui_text(px, FONT_HEADING, (o.left + o.right) / 2, o.top + 25, UI_CENTER, + UI_RGB(0xffffff), "Paused"); + ui_text(px, FONT_SM, (o.left + o.right) / 2, o.top + 43, UI_CENTER, + C_LABEL, "Tap to resume"); + } +} + +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) + return o->period; + o->tick(g); + g->steps++; + flush(g, false); + return o->period; +} + +static void set_paused(game_t *g, bool paused) +{ + if (g->paused == paused) + return; + g->paused = paused; + ui_view_invalidate(&g->view, overlay_rect()); + /* 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); +} + +static bool game_event(ui_view_t *v, const ui_event_t *e) +{ + game_t *g = (game_t *) v; + if (e->kind == UI_TAP || + (e->kind == UI_KEY && (e->ucs4 == ' ' || e->ucs4 == 'p'))) { + set_paused(g, !g->paused); + return true; + } + return false; +} + +static void game_open(game_kind_t kind) +{ + 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); + 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)) { + ui_view_close(&g->view); + return; + } + ui_view_show(&g->view); +} + +void app_snake_open(void) +{ + game_open(GAME_SNAKE); +} + +void app_pong_open(void) +{ + game_open(GAME_PONG); +} + +void app_tetris_open(void) +{ + game_open(GAME_TETRIS); +} + +/* + * Test hooks: state of the game on top, or -1 / false when none + */ +static game_t *top_game(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == game_paint ? (game_t *) v : NULL; +} + +int app_games_test_steps(void) +{ + game_t *g = top_game(); + return g ? g->steps : -1; +} + +int app_games_test_score(void) +{ + game_t *g = top_game(); + return g ? g->score : -1; +} + +int app_games_test_best(void) +{ + game_t *g = top_game(); + return g ? g->best : -1; +} + +int app_games_test_events(void) +{ + game_t *g = top_game(); + return g ? g->events : -1; +} + +int app_games_test_full(void) +{ + game_t *g = top_game(); + return g ? g->full : -1; +} + +bool app_games_test_paused(void) +{ + game_t *g = top_game(); + return g && g->paused; +} diff --git a/apps/cellphone/home.c b/apps/cellphone/home.c new file mode 100644 index 00000000..890d993a --- /dev/null +++ b/apps/cellphone/home.c @@ -0,0 +1,229 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Home screen: pages of app icons. + * + * Paging slides the icon grid by blitting the pixels already on screen and + * painting only the exposed strip, so a swipe costs a memmove per frame + * plus at most one column of icons, whose masks come from the coverage + * cache. + */ + +#include + +#include "phone.h" + +#define COLS 3 +#define ROWS 3 +#define PER_PAGE (COLS * ROWS) +#define CELL_W (PHONE_W / COLS) +#define CELL_H 74 +#define PLATE 40 +#define TILE_INSET 3 /* gap between tiles is twice this */ +#define TILE_R 8 +#define GRID_TOP 6 +#define DOTS_Y (GRID_TOP + ROWS * CELL_H + 12) + +typedef struct { + ui_view_t view; + int page; /* settled page */ + twin_coord_t pan; /* horizontal pan in px: page * PHONE_W - drag */ + twin_coord_t from; /* pan when an animation started */ + twin_coord_t to; + twin_coord_t grab; /* pan a drag starts from */ + bool settling; /* the settle tween is running */ + int pressed; /* app index under the finger, -1 when none */ +} home_t; + +static int pages(void) +{ + return (phone_app_count() + PER_PAGE - 1) / PER_PAGE; +} + +static ui_rect_t grid_rect(void) +{ + return ui_rect(0, GRID_TOP, PHONE_W, ROWS * CELL_H); +} + +/* Cell of app @i on its page, in page-local coordinates */ +static ui_rect_t cell_rect(int i) +{ + int k = i % PER_PAGE; + return ui_rect((k % COLS) * CELL_W, GRID_TOP + (k / COLS) * CELL_H, CELL_W, + CELL_H); +} + +/* Warm-white tile holding an app's icon and label: the touch target */ +static ui_rect_t tile_rect(ui_rect_t cell) +{ + return ui_inset(cell, TILE_INSET); +} + +static ui_rect_t plate_rect(ui_rect_t cell) +{ + return ui_rect(cell.left + (CELL_W - PLATE) / 2, cell.top + 9, PLATE, + PLATE); +} + +static void home_paint(ui_view_t *v, twin_pixmap_t *px) +{ + home_t *h = (home_t *) v; + ui_rect_t clip = ui_view_clip(v); + const phone_theme_t *th = phone_theme(); + phone_paint_wallpaper(px, ui_view_bounds(v), phone_wallpaper()); + + for (int i = 0; i < phone_app_count(); i++) { + ui_rect_t cell = cell_rect(i); + twin_coord_t shift = (i / PER_PAGE) * PHONE_W - h->pan; + cell.left += shift; + cell.right += shift; + if (!ui_intersects(cell, clip)) + continue; + ui_rect_t tile = tile_rect(cell), plate = plate_rect(cell); + bool pressed = i == h->pressed; + /* The ledge shows only along the rounded ends: rasterize those + * bands, not the rows the tile covers */ + ui_rect_t ledge = + ui_rect(tile.left, tile.top + 1, ui_w(tile), ui_h(tile)); + for (int band = 0; band < 2; band++) { + twin_coord_t top = band ? tile.bottom - TILE_R : tile.top; + twin_rect_t saved = ui_clip_push( + px, ui_rect(tile.left, top, ui_w(tile), TILE_R + 1)); + ui_rrect(px, ledge, TILE_R, 0x1c000000); + twin_pixmap_restore_clip(px, saved); + } + ui_rrect(px, tile, TILE_R, + pressed ? ui_mix(th->card, th->primary, 60) : th->card); + phone_app_icon(px, i, plate, pressed); + ui_text_fit(px, FONT_SM, + ui_rect(tile.left + 2, plate.bottom + 2, ui_w(tile) - 4, + tile.bottom - plate.bottom - 4), + UI_CENTER, th->text, phone_app_at(i)->name); + } + + int current = (h->pan + PHONE_W / 2) / PHONE_W; + ui_dots(px, PHONE_W / 2, DOTS_Y, pages(), current); +} + +static int app_at(home_t *h, twin_coord_t x, twin_coord_t y) +{ + for (int i = h->page * PER_PAGE; + i < phone_app_count() && i < (h->page + 1) * PER_PAGE; i++) { + if (ui_hit(tile_rect(cell_rect(i)), x, y)) + return i; + } + return -1; +} + +static void pan_to(home_t *h, twin_coord_t pan) +{ + twin_coord_t d = pan - h->pan; + if (!d) + return; + h->pan = pan; + ui_view_scroll(&h->view, grid_rect(), -d, 0); + ui_view_invalidate(&h->view, ui_rect(0, DOTS_Y - 6, PHONE_W, 12)); +} + +static bool settle_step(ui_view_t *v, twin_fixed_t value) +{ + home_t *h = (home_t *) v; + pan_to(h, ui_lerp(h->from, h->to, value)); + if (value == TWIN_FIXED_ONE) + h->settling = false; + return true; +} + +/* Tile of @app; empty when the app's page is not shown */ +static ui_rect_t shown_tile(ui_view_t *v, int app) +{ + if (app / PER_PAGE != ((home_t *) v)->page) + return (ui_rect_t) {0, 0, 0, 0}; + return tile_rect(cell_rect(app)); +} + +static void set_pressed(home_t *h, int app) +{ + ui_view_set_pressed(&h->view, &h->pressed, app, shown_tile); +} + +static bool home_event(ui_view_t *v, const ui_event_t *e) +{ + home_t *h = (home_t *) v; + switch (e->kind) { + case UI_PRESS: + set_pressed(h, app_at(h, e->x, e->y)); + h->grab = h->pan; + return true; + case UI_DRAG: { + set_pressed(h, -1); + if (h->settling) { + /* Catch the page mid-settle: continue from where it is */ + ui_view_stop_tweens(v); + h->settling = false; + h->grab = h->pan + e->dx; + } + twin_coord_t pan = h->grab - e->dx; + twin_coord_t max = (pages() - 1) * PHONE_W; + /* Rubber band past the first and last page */ + if (pan < 0) + pan /= 3; + else if (pan > max) + pan = max + (pan - max) / 3; + pan_to(h, pan); + return true; + } + case UI_RELEASE: { + int page = h->page; + if ((e->dx < -PHONE_W / 4 || e->vx < -300) && page < pages() - 1) + page++; + else if ((e->dx > PHONE_W / 4 || e->vx > 300) && page > 0) + page--; + h->page = page; + h->from = h->pan; + h->to = page * PHONE_W; + if (h->from != h->to) { + h->settling = ui_view_tween(v, 220, TwinEaseOutCubic, settle_step); + if (!h->settling) + pan_to(h, h->to); /* no animation: settle at once */ + } + return true; + } + case UI_TAP: { + int app = app_at(h, e->x, e->y); + set_pressed(h, -1); + if (app >= 0) + phone_open(app); + return true; + } + case UI_CANCEL: + set_pressed(h, -1); + return true; + default: + return false; + } +} + +ui_view_t *home_create(void) +{ + home_t *h = ui_view_new(sizeof(*h), "Home", home_paint, home_event, NULL); + if (!h) + return NULL; + h->pressed = -1; + return &h->view; +} + +/* Test hook: @app's tile in home-screen content coordinates; false unless + * home is on top with that tile's page shown */ +bool app_home_icon_rect(int app, ui_rect_t *out) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != home_paint || app < 0 || app >= phone_app_count()) + return false; + *out = shown_tile(v, app); + return out->right > out->left; +} diff --git a/apps/cellphone/lock.c b/apps/cellphone/lock.c new file mode 100644 index 00000000..bf6fd4b5 --- /dev/null +++ b/apps/cellphone/lock.c @@ -0,0 +1,172 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Lock screen: clock, date, slide to unlock. + * + * The hint text breathes on a timer that repaints only its own rectangle, + * and the clock repaints once a minute; the rest of the screen is never + * touched while the phone sits locked. + */ + +#include "phone.h" + +#define TRACK_X 20 +#define TRACK_W (PHONE_W - 2 * TRACK_X) +#define TRACK_Y 284 +#define TRACK_H 22 +#define KNOB_R 11 +#define HINT_Y 266 +#define TIME_Y 74 +#define DATE_Y 104 + +typedef struct { + ui_view_t view; + twin_coord_t knob; /* knob travel in px, 0 .. TRACK_W - 2 * KNOB_R */ + twin_coord_t from; /* knob position when a spring-back started */ + bool grabbed; + int phase; /* breathing phase, 0..4095 */ + twin_a8_t hint_alpha; + int minute; +} lock_t; + +static twin_coord_t travel(void) +{ + return TRACK_W - 2 * KNOB_R - 4; +} + +static ui_rect_t hint_rect(void) +{ + return ui_rect(40, HINT_Y - 14, PHONE_W - 80, 20); +} + +static ui_rect_t track_rect(void) +{ + return ui_rect(TRACK_X, TRACK_Y, TRACK_W, TRACK_H); +} + +static ui_rect_t clock_rect(void) +{ + return ui_rect(0, TIME_Y - 36, PHONE_W, DATE_Y - TIME_Y + 46); +} + +static void lock_paint(ui_view_t *v, twin_pixmap_t *px) +{ + lock_t *l = (lock_t *) v; + ui_vgrad(px, ui_view_bounds(v), UI_RGB(0x2a1414), UI_RGB(0x050505)); + + char buf[48]; + phone_format_time(buf, sizeof(buf)); + ui_text(px, FONT_CLOCK, PHONE_W / 2, TIME_Y, UI_CENTER, UI_RGB(0xffffff), + buf); + phone_format_date(buf, sizeof(buf)); + ui_text(px, FONT_NORMAL, PHONE_W / 2, DATE_Y, UI_CENTER, UI_RGB(0xe6e6e6), + buf); + + ui_text(px, FONT_SM, PHONE_W / 2, HINT_Y, UI_CENTER, + ui_alpha(UI_RGB(0xd0d0d0), l->hint_alpha), "Slide to unlock"); + + ui_rect_t t = track_rect(); + ui_rrect(px, t, TRACK_H / 2, ui_alpha(UI_RGB(0xffffff), 0x40)); + /* Lit trail behind the knob */ + ui_rrect(px, ui_rect(t.left, t.top, l->knob + 2 * KNOB_R + 4, TRACK_H), + TRACK_H / 2, ui_alpha(UI_RGB(0x22c55e), 0x30)); + twin_coord_t kx = t.left + 2 + KNOB_R + l->knob; + twin_coord_t ky = t.top + TRACK_H / 2; + ui_circle(px, kx, ky + 1, KNOB_R + 1, 0x50000000); + ui_circle(px, kx, ky, KNOB_R, UI_RGB(0xffffff)); + ui_icon(px, TWIN_SYM_RIGHT, FONT_SM, kx + 1, ky, UI_RGB(0x22c55e)); +} + +static twin_time_t lock_tick(ui_view_t *v) +{ + lock_t *l = (lock_t *) v; + /* Breathe between 30% and 80% over ~1.8 s */ + l->phase = (l->phase + 4096 * 60 / 1800) & 4095; + twin_fixed_t c; + twin_sincos((twin_angle_t) l->phase, NULL, &c); + int a = + 0x4d + (int) (((int64_t) (0xcc - 0x4d) * (TWIN_FIXED_ONE - c)) >> 17); + if (a != l->hint_alpha) { + l->hint_alpha = (twin_a8_t) a; + ui_view_invalidate(v, hint_rect()); + } + + phone_clock_t clk; + phone_clock(&clk); + int minute = clk.hour * 60 + clk.min; + if (minute != l->minute) { + l->minute = minute; + ui_view_invalidate(v, clock_rect()); + } + return 60; +} + +static void set_knob(lock_t *l, twin_coord_t knob) +{ + if (knob < 0) + knob = 0; + if (knob > travel()) + knob = travel(); + if (knob == l->knob) + return; + l->knob = knob; + ui_view_invalidate(&l->view, ui_inset(track_rect(), -3)); +} + +static bool spring_back(ui_view_t *v, twin_fixed_t value) +{ + lock_t *l = (lock_t *) v; + set_knob(l, l->from - twin_fixed_to_int(l->from * value)); + return true; +} + +static bool lock_event(ui_view_t *v, const ui_event_t *e) +{ + lock_t *l = (lock_t *) v; + ui_rect_t t = ui_inset(track_rect(), -10); + switch (e->kind) { + case UI_PRESS: + l->grabbed = ui_hit(t, e->x, e->y); + /* A new slide takes the knob from a running spring-back */ + if (l->grabbed) + ui_view_stop_tweens(v); + return true; + case UI_MOVE: + case UI_DRAG: + if (l->grabbed) + set_knob(l, e->dx); + return true; + case UI_RELEASE: + case UI_TAP: + if (!l->grabbed) + return true; + l->grabbed = false; + if (l->knob * 10 >= travel() * 8) { + /* Unlocked: the window manager slides the lock away */ + ui_view_close(v); + return true; + } + l->from = l->knob; + if (l->from && !ui_view_tween(v, 180, TwinEaseOutCubic, spring_back)) + set_knob(l, 0); + return true; + default: + return false; + } +} + +ui_view_t *lock_create(void) +{ + lock_t *l = ui_view_new_fullscreen(sizeof(*l), "Lock", lock_paint, + lock_event, NULL); + if (!l) + return NULL; + l->hint_alpha = 0x4d; + l->minute = -1; + ui_view_every(&l->view, 60, lock_tick); + return &l->view; +} diff --git a/apps/cellphone/music.c b/apps/cellphone/music.c new file mode 100644 index 00000000..0315e19b --- /dev/null +++ b/apps/cellphone/music.c @@ -0,0 +1,894 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Music: a track list and a now-playing screen over a simulated player. + * + * The player is a single static struct, the one allowed global, standing in + * for a media service that outlives the app. It holds no timer: while + * playing it records the clock time at which the current track's position + * was zero, so the position is derived from twin_now() on demand and + * playback keeps "running" while the app is closed at zero cost. + * player_sync() rolls finished tracks over whenever someone looks. + * + * Views only own a once-per-second timer that samples the player into a + * snapshot and invalidates what moved (knob, time labels, a progress line, + * a row on track change). Paint draws from the snapshot only, so every + * pixel change is paired with an invalidation; a full repaint (reveal, + * theme switch) resamples first. Album art is procedural: a diagonal + * gradient keyed off album and track, vinyl rings and a note glyph. + */ + +#include + +#include "data.h" +#include "phone.h" + + +/* + * Player service + */ +static struct { + int track; + bool playing, shuffle, repeat; + twin_time_t start; /* playing: clock time at position 0 of the track */ + int32_t pos; /* paused: position in ms */ + uint32_t seed; /* shuffle generator */ +} player = {.seed = 0x9e3779b9u}; + +static int32_t track_ms(int i) +{ + return (int32_t) phone_tracks()[i].duration_sec * 1000; +} + +/* twin_now() wraps: do clock arithmetic in unsigned and convert back */ +static int32_t player_elapsed(void) +{ + return (int32_t) ((uint32_t) twin_now() - (uint32_t) player.start); +} + +static twin_time_t time_add(twin_time_t t, int32_t ms) +{ + return (twin_time_t) ((uint32_t) t + (uint32_t) ms); +} + +static int32_t player_pos(void) +{ + return player.playing ? player_elapsed() : player.pos; +} + +static int shuffle_pick(void) +{ + /* xorshift32; any track but the current one */ + uint32_t x = player.seed; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + player.seed = x; + int i = (int) (x % (PHONE_TRACK_COUNT - 1)); + return i >= player.track ? i + 1 : i; +} + +static int next_track(void) +{ + return player.shuffle ? shuffle_pick() + : (player.track + 1) % PHONE_TRACK_COUNT; +} + +/* Catch up with the clock: roll over every track that finished */ +static void player_sync(void) +{ + if (!player.playing) + return; + while (player_elapsed() >= track_ms(player.track)) { + player.start = time_add(player.start, track_ms(player.track)); + if (!player.repeat) + player.track = next_track(); + } +} + +static void player_seek(int32_t ms) +{ + int32_t dur = track_ms(player.track); + if (ms < 0) + ms = 0; + if (ms > dur - 1) + ms = dur - 1; + if (player.playing) + player.start = time_add(twin_now(), -ms); + else + player.pos = ms; +} + +static void player_play(bool on) +{ + player_sync(); + if (on == player.playing) + return; + int32_t pos = player_pos(); + player.playing = on; + player_seek(pos); +} + +static void player_select(int track) +{ + player_sync(); + player.track = track; + player_seek(0); + player_play(true); +} + +static void player_skip(int dir) +{ + player_sync(); + player_select(dir > 0 ? next_track() + : (player.track + PHONE_TRACK_COUNT - 1) % + PHONE_TRACK_COUNT); +} + +/* Delay until the displayed second changes */ +static twin_time_t player_next_second(void) +{ + return 1000 - player_pos() % 1000; +} + +static void format_time(char *buf, size_t len, const char *sign, int sec) +{ + snprintf(buf, len, "%s%d:%02d", sign, sec / 60, sec % 60); +} + +/* + * Procedural album art + */ +static twin_argb32_t hsv(int h, int s, int v) +{ + /* h 0..359, s and v 0..255 */ + int f = (h % 60) * 255 / 60; + int p = v * (255 - s) / 255; + int q = v * (255 - s * f / 255) / 255; + int t = v * (255 - s * (255 - f) / 255) / 255; + int r, g, b; + switch (h / 60) { + case 0: + r = v, g = t, b = p; + break; + case 1: + r = q, g = v, b = p; + break; + case 2: + r = p, g = v, b = t; + break; + case 3: + r = p, g = q, b = v; + break; + case 4: + r = t, g = p, b = v; + break; + default: + r = v, g = p, b = q; + break; + } + return UI_RGB((uint32_t) r << 16 | (uint32_t) g << 8 | (uint32_t) b); +} + +/* Albums get distinct base hues; tracks shift within their album's family */ +static void art_colors(int track, twin_argb32_t *c0, twin_argb32_t *c1) +{ + unsigned h = 0; + for (const char *s = phone_tracks()[track].album; *s; s++) + h = h * 31 + (unsigned char) *s; + int hue = (int) ((h % 6) * 60 + (unsigned) track * 17) % 360; + *c0 = hsv(hue, 140, 235); + *c1 = hsv((hue + 45) % 360, 220, 130); +} + +static void paint_art(twin_pixmap_t *px, ui_rect_t r, int track, int radius) +{ + twin_argb32_t c0, c1; + art_colors(track, &c0, &c1); + twin_path_t *p = ui_path(); + twin_path_rounded_rectangle(p, ui_fx(r.left), ui_fx(r.top), ui_fx(ui_w(r)), + ui_fx(ui_h(r)), ui_fx(radius), ui_fx(radius)); + twin_paint_path_linear(px, p, ui_fx(r.left), ui_fx(r.top), c0, + ui_fx(r.right), ui_fx(r.bottom), c1); + ui_path_done(p); + + int s = ui_w(r); + twin_coord_t cx = (r.left + r.right) / 2, cy = (r.top + r.bottom) / 2; + if (s >= 64) { + /* Vinyl grooves behind the note */ + for (int k = 0; k < 4; k++) + ui_ring(px, cx, cy, s * (44 - k * 7) / 100, 1, + ui_alpha(UI_WHITE, (twin_a8_t) (0x30 - k * 6))); + ui_circle(px, cx, cy, s * 16 / 100, ui_alpha(UI_BLACK, 0x18)); + } + ui_icon(px, TWIN_SYM_MUSIC, s * 2 / 5, cx, cy, ui_alpha(UI_WHITE, 0xe8)); +} + +/* + * Now playing screen + */ +#define ART 116 +#define ART_TOP 8 +#define TINT_H 140 +#define TITLE_BASE 146 +#define SUB_BASE 164 +#define SLIDER_Y 187 /* center of the progress row */ +#define CTL_Y 230 + +enum { C_SHUFFLE, C_PREV, C_PLAY, C_NEXT, C_REPEAT, C_COUNT }; + +typedef struct { + ui_view_t view; + /* Snapshot of the player as last painted */ + int track; + bool playing; + int value; /* slider 0..1000 */ + int sec; /* elapsed seconds */ + bool seeking; /* finger on the slider: value follows it */ + int pressed; /* control under the finger, -1 when none */ +} np_t; + +static ui_rect_t art_rect(void) +{ + return ui_rect((PHONE_W - ART) / 2, ART_TOP, ART, ART); +} + +static ui_rect_t info_rect(void) +{ + return ui_rect(0, TINT_H - 12, PHONE_W, SUB_BASE + 6 - (TINT_H - 12)); +} + +static ui_rect_t slider_rect(void) +{ + /* Times flank the bar on one row so a tick damages one thin band */ + return ui_rect(44, SLIDER_Y - 11, PHONE_W - 88, 22); +} + +/* Generous touch target around the thin slider track */ +static ui_rect_t slider_hit(void) +{ + ui_rect_t r = slider_rect(); + r.top -= 6; + r.bottom += 6; + return r; +} + +static ui_rect_t elapsed_rect(void) +{ + return ui_rect(8, SLIDER_Y - 7, 36, 14); +} + +static ui_rect_t remain_rect(void) +{ + return ui_rect(PHONE_W - 44, SLIDER_Y - 7, 36, 14); +} + +static const twin_coord_t ctl_x[C_COUNT] = {28, 74, 120, 166, 212}; + +static ui_rect_t ctl_rect(int c) +{ + int s = c == C_PLAY ? 52 : 40; + return ui_rect(ctl_x[c] - s / 2, CTL_Y - s / 2, s, s); +} + +static int ctl_at(twin_coord_t x, twin_coord_t y) +{ + for (int c = 0; c < C_COUNT; c++) + if (ui_hit(ctl_rect(c), x, y)) + return c; + return -1; +} + +static twin_coord_t knob_x(int value) +{ + return ui_slider_knob_x(slider_rect(), value); +} + +static void np_sample(np_t *n, int *value, int *sec) +{ + int32_t dur = track_ms(n->track); + int32_t pos = player_pos(); + if (n->seeking) { + *value = n->value; + pos = (int32_t) ((int64_t) n->value * dur / 1000); + } else { + if (pos > dur) + pos = dur; + *value = (int) ((int64_t) pos * 1000 / dur); + } + *sec = pos / 1000; +} + +/* Slider band swept by the knob (with its shadow) moving from a to b */ +static void np_invalidate_knob(np_t *n, int from, int to) +{ + twin_coord_t a = knob_x(from), b = knob_x(to); + ui_rect_t s = slider_rect(); + twin_coord_t l = (a < b ? a : b) - 12, r = (a < b ? b : a) + 12; + ui_view_invalidate(&n->view, ui_rect(l, s.top, r - l, ui_h(s))); +} + +static void np_snapshot(np_t *n) +{ + player_sync(); + n->track = player.track; + n->playing = player.playing; + np_sample(n, &n->value, &n->sec); +} + +/* Tick only while playing; a paused player has nothing to animate */ +static void ensure_tick(ui_view_t *v, ui_timer_t tick) +{ + if (player.playing && !ui_view_has_timer(v, tick)) + ui_view_every(v, player_next_second(), tick); +} + +static twin_time_t np_tick(ui_view_t *v); + +/* Re-sample the player, invalidate only what changed, and keep the tick + * running while it plays */ +static void np_refresh(np_t *n) +{ + ui_view_t *v = &n->view; + player_sync(); + ensure_tick(v, np_tick); + if (player.track != n->track) { + np_snapshot(n); + ui_view_invalidate_all(v); + return; + } + if (player.playing != n->playing) { + n->playing = player.playing; + ui_view_invalidate(v, ctl_rect(C_PLAY)); + } + int value, sec; + np_sample(n, &value, &sec); + if (value != n->value) { + np_invalidate_knob(n, n->value, value); + n->value = value; + } + if (sec != n->sec) { + n->sec = sec; + ui_view_invalidate(v, elapsed_rect()); + ui_view_invalidate(v, remain_rect()); + } +} + +static twin_time_t np_tick(ui_view_t *v) +{ + np_refresh((np_t *) v); + return player.playing ? player_next_second() : -1; +} + +static void paint_control(twin_pixmap_t *px, np_t *n, int c) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = ctl_rect(c); + twin_coord_t cx = ctl_x[c]; + bool down = c == n->pressed; + if (c == C_PLAY) { + ui_circle(px, cx, CTL_Y + 2, 25, ui_alpha(UI_BLACK, 0x28)); + ui_circle(px, cx, CTL_Y, 24, down ? th->primary_dk : th->primary); + /* The play triangle's mass sits left of its box: nudge it right */ + ui_icon(px, n->playing ? TWIN_SYM_PAUSE : TWIN_SYM_PLAY, 20, + cx + (n->playing ? 0 : 2), CTL_Y, th->on_primary); + return; + } + if (down) + ui_circle(px, cx, CTL_Y, ui_w(r) / 2 - 2, + ui_mix(th->bg, th->primary, 70)); + if (c == C_PREV || c == C_NEXT) { + ui_icon(px, c == C_PREV ? TWIN_SYM_PREV : TWIN_SYM_NEXT, 18, cx, CTL_Y, + th->text); + return; + } + bool on = c == C_SHUFFLE ? player.shuffle : player.repeat; + ui_icon(px, c == C_SHUFFLE ? TWIN_SYM_SHUFFLE : TWIN_SYM_REPEAT, 15, cx, + CTL_Y - 1, on ? th->primary : th->text_sec); + if (on) + ui_circle(px, cx, CTL_Y + 13, 2, th->primary); +} + +static void np_paint(ui_view_t *v, twin_pixmap_t *px) +{ + np_t *n = (np_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v), bounds = ui_view_bounds(v); + if (clip.left <= bounds.left && clip.top <= bounds.top && + clip.right >= bounds.right && clip.bottom >= bounds.bottom) + np_snapshot(n); + + const phone_track_t *t = &phone_tracks()[n->track]; + twin_argb32_t c0, c1; + art_colors(n->track, &c0, &c1); + + /* Background washed with the cover's color, fading into the theme */ + ui_rect_t tint = ui_rect(0, 0, PHONE_W, TINT_H); + if (ui_intersects(clip, tint)) + ui_vgrad(px, tint, ui_mix(th->bg, c0, 110), th->bg); + ui_fill(px, ui_rect(0, TINT_H, PHONE_W, ui_h(bounds) - TINT_H), th->bg); + + ui_rect_t art = art_rect(); + if (ui_intersects(clip, ui_inset(art, -6))) { + ui_rrect(px, ui_rect(art.left + 2, art.top + 4, ART - 4, ART), 14, + ui_alpha(UI_BLACK, 0x30)); + paint_art(px, art, n->track, 14); + } + + if (ui_intersects(clip, info_rect())) { + char sub[96]; + snprintf(sub, sizeof(sub), "%s \xc2\xb7 %s", t->artist, t->album); + ui_text_fit(px, FONT_HEADING, + ui_rect(8, TITLE_BASE - 15, PHONE_W - 16, 20), UI_CENTER, + th->text, t->title); + ui_text_fit(px, FONT_SM, ui_rect(8, SUB_BASE - 12, PHONE_W - 16, 16), + UI_CENTER, th->text_sec, sub); + } + + if (ui_intersects(clip, slider_rect())) + ui_slider(px, slider_rect(), n->value, n->seeking); + + char buf[16]; + if (ui_intersects(clip, elapsed_rect())) { + format_time(buf, sizeof(buf), "", n->sec); + ui_text(px, FONT_SM, elapsed_rect().right - 2, SLIDER_Y + 4, UI_RIGHT, + th->text_sec, buf); + } + if (ui_intersects(clip, remain_rect())) { + int left = (int) t->duration_sec - n->sec; + format_time(buf, sizeof(buf), "-", left > 0 ? left : 0); + ui_text(px, FONT_SM, remain_rect().left + 2, SLIDER_Y + 4, UI_LEFT, + th->text_sec, buf); + } + + for (int c = 0; c < C_COUNT; c++) + if (ui_intersects(clip, ctl_rect(c))) + paint_control(px, n, c); +} + +static ui_rect_t np_ctl(ui_view_t *v, int c) +{ + (void) v; + return ctl_rect(c); +} + +static void np_set_pressed(np_t *n, int c) +{ + ui_view_set_pressed(&n->view, &n->pressed, c, np_ctl); +} + +static void np_seek_to(np_t *n, twin_coord_t x) +{ + int value = ui_slider_value_at(slider_rect(), x); + if (value == n->value) + return; + np_invalidate_knob(n, n->value, value); + n->value = value; + np_refresh(n); /* time labels follow the finger */ +} + +static void np_action(np_t *n, int c) +{ + switch (c) { + case C_PLAY: + player_play(!player.playing); + break; + case C_PREV: + case C_NEXT: + player_skip(c == C_NEXT ? 1 : -1); + break; + case C_SHUFFLE: + player.shuffle = !player.shuffle; + ui_view_invalidate(&n->view, ctl_rect(c)); + break; + case C_REPEAT: + player.repeat = !player.repeat; + ui_view_invalidate(&n->view, ctl_rect(c)); + break; + } + np_refresh(n); +} + +static bool np_event(ui_view_t *v, const ui_event_t *e) +{ + np_t *n = (np_t *) v; + switch (e->kind) { + case UI_SHOW: + /* The player may have changed while another view covered this one, + * and a covered view keeps its old pixels unless it was evicted */ + np_refresh(n); + return true; + case UI_PRESS: + if (ui_hit(slider_hit(), e->x, e->y)) { + n->seeking = true; + ui_view_invalidate(v, slider_rect()); /* knob grows */ + np_seek_to(n, e->x); + return true; + } + np_set_pressed(n, ctl_at(e->x, e->y)); + return true; + case UI_MOVE: + case UI_DRAG: + if (n->seeking) + np_seek_to(n, e->x); + else + np_set_pressed(n, e->kind == UI_MOVE ? ctl_at(e->x, e->y) : -1); + return true; + case UI_TAP: + case UI_RELEASE: + if (n->seeking) { + np_seek_to(n, e->x); + player_seek( + (int32_t) ((int64_t) n->value * track_ms(n->track) / 1000)); + n->seeking = false; + ui_view_invalidate(v, slider_rect()); /* knob shrinks */ + np_refresh(n); + return true; + } + np_set_pressed(n, -1); + if (e->kind == UI_TAP) { + int c = ctl_at(e->x, e->y); + if (c >= 0) + np_action(n, c); + } + return true; + case UI_CANCEL: + if (n->seeking) { + n->seeking = false; + ui_view_invalidate(v, slider_rect()); + np_refresh(n); + } + np_set_pressed(n, -1); + return false; + default: + return false; + } +} + +static void now_playing_open(void) +{ + np_t *n = ui_view_new(sizeof(*n), "Now Playing", np_paint, np_event, NULL); + if (!n) + return; + n->pressed = -1; + np_snapshot(n); + ensure_tick(&n->view, np_tick); + ui_view_show(&n->view); +} + +/* + * Track list + */ +#define ROW_H PHONE_LIST_ROW_H +#define MINI_H 48 +#define LIST_TOP PHONE_HEADER_H +#define NUM_W 36 + +enum { HIT_MINI = -2, HIT_PLAY = -3, HIT_NONE = -1 }; + +typedef struct { + ui_view_t view; + ui_scroll_t scroll; + int pressed; /* row, HIT_MINI, HIT_PLAY or HIT_NONE */ + /* Snapshot of the player as last painted */ + int track; + bool playing; + twin_coord_t fill; /* mini bar progress width */ +} list_t; + +/* Top of the mini player, which sits at the bottom of the view */ +static twin_coord_t mini_y(const list_t *l) +{ + return ui_view_height(&l->view) - MINI_H; +} + +static ui_rect_t list_area(const list_t *l) +{ + return ui_rect(0, LIST_TOP, PHONE_W, mini_y(l) - LIST_TOP); +} + +static void list_layout(ui_view_t *v) +{ + list_t *l = (list_t *) v; + ui_scroll_setup(&l->scroll, list_area(l), PHONE_TRACK_COUNT * ROW_H); +} + +static ui_rect_t mini_rect(const list_t *l) +{ + return ui_rect(0, mini_y(l), PHONE_W, MINI_H); +} + +static ui_rect_t mini_play_rect(const list_t *l) +{ + return ui_rect(PHONE_W - 48, mini_y(l) + 2, 48, MINI_H - 2); +} + +static ui_rect_t row_rect(const list_t *l, int i) +{ + return ui_rect(0, LIST_TOP + i * ROW_H - l->scroll.offset, PHONE_W, ROW_H); +} + +/* Part of row @i inside the list viewport */ +static ui_rect_t row_visible(const list_t *l, int i) +{ + ui_rect_t r = row_rect(l, i), a = list_area(l); + if (r.top < a.top) + r.top = a.top; + if (r.bottom > a.bottom) + r.bottom = a.bottom; + return r; +} + +static twin_coord_t mini_fill(void) +{ + int32_t dur = track_ms(player.track), pos = player_pos(); + if (pos > dur) + pos = dur; + return (twin_coord_t) ((int64_t) pos * PHONE_W / dur); +} + +static void list_snapshot(list_t *l) +{ + player_sync(); + l->track = player.track; + l->playing = player.playing; + l->fill = mini_fill(); +} + +static twin_time_t list_tick(ui_view_t *v); + +/* As np_refresh(), for the list's rows and mini player */ +static void list_refresh(list_t *l) +{ + ui_view_t *v = &l->view; + player_sync(); + ensure_tick(v, list_tick); + if (player.track != l->track) { + ui_view_invalidate(v, row_visible(l, l->track)); + ui_view_invalidate(v, row_visible(l, player.track)); + ui_view_invalidate(v, mini_rect(l)); + list_snapshot(l); + return; + } + if (player.playing != l->playing) { + l->playing = player.playing; + ui_view_invalidate(v, row_visible(l, l->track)); /* note vs number */ + ui_view_invalidate(v, mini_play_rect(l)); + } + twin_coord_t fill = mini_fill(); + if (fill != l->fill) { + twin_coord_t a = fill < l->fill ? fill : l->fill; + twin_coord_t b = fill < l->fill ? l->fill : fill; + ui_view_invalidate(v, ui_rect(a, mini_y(l), b - a, 2)); + l->fill = fill; + } +} + +static twin_time_t list_tick(ui_view_t *v) +{ + list_refresh((list_t *) v); + return player.playing ? player_next_second() : -1; +} + +static void paint_row(twin_pixmap_t *px, list_t *l, int i) +{ + const phone_theme_t *th = phone_theme(); + const phone_track_t *t = &phone_tracks()[i]; + ui_rect_t r = row_rect(l, i); + bool current = i == l->track; + twin_argb32_t bg = th->card; + if (i == l->pressed) + bg = ui_mix(th->card, th->primary, 70); + else if (current) + bg = ui_mix(th->card, th->primary, 34); + ui_fill(px, r, bg); + + twin_coord_t cy = (r.top + r.bottom) / 2; + if (current && l->playing) { + ui_icon(px, TWIN_SYM_MUSIC, 14, NUM_W / 2 + 2, cy, th->primary); + } else { + char num[12]; + snprintf(num, sizeof(num), "%d", i + 1); + ui_text(px, FONT_SM, NUM_W / 2 + 2, ui_baseline(FONT_SM, r.top, ROW_H), + UI_CENTER, current ? th->primary : th->text_sec, num); + } + + char dur[16]; + format_time(dur, sizeof(dur), "", (int) t->duration_sec); + twin_coord_t dw = ui_text_width(FONT_SM, dur); + ui_text(px, FONT_SM, r.right - 10, ui_baseline(FONT_SM, r.top, ROW_H), + UI_RIGHT, th->text_sec, dur); + + ui_rect_t text = {NUM_W - 4, (twin_coord_t) (r.right - dw - 14), r.top, + r.bottom}; + ui_text_fit(px, FONT_NORMAL, + ui_rect(text.left, r.top + 3, ui_w(text), ROW_H / 2 - 2), + UI_LEFT, current ? th->primary : th->text, t->title); + ui_text_fit( + px, FONT_SM, + ui_rect(text.left, r.top + ROW_H / 2 - 1, ui_w(text), ROW_H / 2 - 2), + UI_LEFT, th->text_sec, t->artist); + ui_separator(px, ui_rect(NUM_W, r.top, PHONE_W - NUM_W, ROW_H)); +} + +static void paint_mini(twin_pixmap_t *px, list_t *l, ui_rect_t clip) +{ + const phone_theme_t *th = phone_theme(); + const phone_track_t *t = &phone_tracks()[l->track]; + ui_rect_t m = mini_rect(l); + twin_coord_t my = m.top; + + bool body_down = l->pressed == HIT_MINI; + twin_argb32_t top = + body_down ? ui_mix(th->card, th->primary, 70) : th->card; + ui_vgrad(px, m, top, ui_mix(th->card, th->bg, 110)); + ui_fill(px, ui_rect(0, my, PHONE_W, 2), th->indicator); /* progress */ + ui_fill(px, ui_rect(0, my, l->fill, 2), th->primary); + + if (ui_intersects(clip, ui_rect(0, my + 2, PHONE_W - 48, MINI_H - 2))) { + paint_art(px, ui_rect(8, my + 8, 34, 34), l->track, 6); + twin_coord_t x = 46, w = PHONE_W - 48 - x; + ui_text_fit(px, FONT_NORMAL, ui_rect(x, my + 6, w, 20), UI_LEFT, + th->text, t->title); + ui_text_fit(px, FONT_SM, ui_rect(x, my + 24, w, 18), UI_LEFT, + th->text_sec, t->artist); + } + + ui_rect_t p = mini_play_rect(l); + if (ui_intersects(clip, p)) { + twin_coord_t cx = (p.left + p.right) / 2, cy = (p.top + p.bottom) / 2; + ui_circle(px, cx, cy, 16, + l->pressed == HIT_PLAY ? th->primary_dk : th->primary); + ui_icon(px, l->playing ? TWIN_SYM_PAUSE : TWIN_SYM_PLAY, 13, + cx + (l->playing ? 0 : 1), cy, th->on_primary); + } +} + +static void list_paint(ui_view_t *v, twin_pixmap_t *px) +{ + list_t *l = (list_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v), bounds = ui_view_bounds(v); + if (clip.left <= bounds.left && clip.top <= bounds.top && + clip.right >= bounds.right && clip.bottom >= bounds.bottom) + list_snapshot(l); + + ui_rect_t area = list_area(l); + for (int i = 0; i < PHONE_TRACK_COUNT; i++) + if (ui_intersects(clip, row_visible(l, i))) + paint_row(px, l, i); + /* Below the last row when the list is shorter than the viewport */ + twin_coord_t end = row_rect(l, PHONE_TRACK_COUNT - 1).bottom; + if (end < area.bottom) + ui_fill(px, ui_rect(0, end, PHONE_W, area.bottom - end), th->card); + ui_scroll_bar(px, &l->scroll); + + /* Header and mini player sit on top of rows that scrolled under them */ + ui_rect_t hdr = ui_rect(0, 0, PHONE_W, LIST_TOP); + if (ui_intersects(clip, hdr)) { + char count[16]; + snprintf(count, sizeof(count), "%d songs", PHONE_TRACK_COUNT); + ui_header(px, hdr, NULL, "Music", count); + } + if (ui_intersects(clip, mini_rect(l))) + paint_mini(px, l, clip); +} + +static int list_hit(const list_t *l, twin_coord_t x, twin_coord_t y) +{ + if (ui_hit(mini_play_rect(l), x, y)) + return HIT_PLAY; + if (ui_hit(mini_rect(l), x, y)) + return HIT_MINI; + if (!ui_hit(list_area(l), x, y)) + return HIT_NONE; + int i = (y - LIST_TOP + l->scroll.offset) / ROW_H; + return i < PHONE_TRACK_COUNT ? i : HIT_NONE; +} + +static void list_invalidate_hit(list_t *l, int h) +{ + if (h >= 0) + ui_view_invalidate(&l->view, row_visible(l, h)); + else if (h == HIT_PLAY) + ui_view_invalidate(&l->view, mini_play_rect(l)); + else if (h == HIT_MINI) + ui_view_invalidate(&l->view, mini_rect(l)); +} + +static void list_set_pressed(list_t *l, int h) +{ + if (h == l->pressed) + return; + list_invalidate_hit(l, l->pressed); + l->pressed = h; + list_invalidate_hit(l, h); +} + +static bool list_event(ui_view_t *v, const ui_event_t *e) +{ + list_t *l = (list_t *) v; + if (e->kind == UI_SHOW) { + /* Now Playing may have changed the song while it covered the list */ + list_refresh(l); + return true; + } + if (ui_scroll_event(v, &l->scroll, e)) { + list_set_pressed(l, HIT_NONE); + return true; + } + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + list_set_pressed(l, list_hit(l, e->x, e->y)); + return true; + case UI_TAP: { + int h = list_hit(l, e->x, e->y); + list_set_pressed(l, HIT_NONE); + if (h == HIT_PLAY) { + player_play(!player.playing); + list_refresh(l); + } else if (h == HIT_MINI || h >= 0) { + /* Tapping the current song resumes it rather than restarting */ + if (h >= 0 && h != player.track) + player_select(h); + else if (h >= 0) + player_play(true); + list_refresh(l); + now_playing_open(); + } + return true; + } + default: + list_set_pressed(l, HIT_NONE); + return false; + } +} + +void app_music_open(void) +{ + list_t *l = ui_view_new(sizeof(*l), "Music", list_paint, list_event, NULL); + if (!l) + return; + l->pressed = HIT_NONE; + list_snapshot(l); + ui_view_set_layout(&l->view, list_layout); + /* Start with the current song in view */ + l->scroll.offset = (l->track - 1) * ROW_H; + list_layout(&l->view); /* clamps the offset */ + ensure_tick(&l->view, list_tick); + ui_view_show(&l->view); +} + +/* + * Test hooks + */ +int app_music_test_elapsed(void) +{ + player_sync(); + return player_pos() / 1000; +} + +int app_music_test_track(void) +{ + player_sync(); + return player.track; +} + +bool app_music_test_playing(void) +{ + return player.playing; +} + +/* Scroll offset of the track list on top, -1 when it is not on top */ +int app_music_test_scroll(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != list_paint) + return -1; + return ((list_t *) v)->scroll.offset; +} diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h new file mode 100644 index 00000000..6dc5fdfb --- /dev/null +++ b/apps/cellphone/phone.h @@ -0,0 +1,720 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone demo: shell and UI kit + * + * Every screen is a "view": one twin window holding the screen's pixels, + * stacked by the mobile window manager. Views paint immediate-mode with + * the helpers below; there is no per-widget object tree. A screen's state + * is a plain C struct that embeds ui_view_t as its first member. + * + * typedef struct { ui_view_t view; int count; } counter_t; + * + * static void counter_paint(ui_view_t *v, twin_pixmap_t *px) + * { + * counter_t *c = (counter_t *) v; + * ui_fill(px, ui_view_bounds(v), phone_theme()->bg); + * ... + * } + * static bool counter_event(ui_view_t *v, const ui_event_t *e) + * { + * if (e->kind == UI_TAP) { ((counter_t *) v)->count++; + * ui_view_invalidate_all(v); return true; } + * return false; + * } + * void app_counter_open(void) + * { + * counter_t *c = ui_view_new(sizeof(*c), "Counter", counter_paint, + * counter_event, NULL); + * if (c) ui_view_show(&c->view); + * } + * + * Coordinates handed to paint() and event() are client coordinates: (0, 0) + * is the top-left of the view. Drawing is clipped to the damaged region; + * redraw everything and let the clip (and the coverage cache) make it cheap, + * or skip work outside ui_view_clip(). + * + * Memory rules: views free their own state in destroy(); timers and tweens + * started with ui_view_every()/ui_view_tween() die with the view. Never + * keep raw pointers to another view. + */ + +#ifndef _APPS_PHONE_H_ +#define _APPS_PHONE_H_ + +#include +#include +#include +#include + +/* + * Geometry: portrait QVGA + */ +#define PHONE_W 240 +#define PHONE_H 320 +#define PHONE_STATUS_H 20 +#define PHONE_NAV_H 42 +#define PHONE_CONTENT_H (PHONE_H - PHONE_STATUS_H - PHONE_NAV_H) /* 258 */ + +/* + * PHONE_CONTENT_H is the view height between the mobile bars only: under + * the other scenes a view is taller or shorter. Lay out from + * ui_view_height() of the view at hand. + */ + +/* Pixel format of every phone surface */ +#if defined(CONFIG_DEMO_CELLPHONE_RGB16) +#define PHONE_FORMAT TWIN_RGB16 +#else +#define PHONE_FORMAT TWIN_ARGB32 +#endif + +#define PHONE_LIST_ROW_H 40 +#define PHONE_SECTION_H 22 +#define PHONE_HEADER_H 30 +#define PHONE_PAD 8 + +/* + * Theme + */ +/* + * Three surfaces carry the structure: the page and its content (bg, card), + * grouped areas a step darker (group: section labels, icon columns, the + * navigation tray), and the deep bars (status bar, headers) with on_bar + * text. Colors keep one meaning each: primary marks the selection and the + * main action, call_red a destructive one. + */ +typedef struct { + const char *name; + twin_argb32_t primary, primary_dk; + twin_argb32_t statusbar, statusbar_grad; /* bottom, top */ + twin_argb32_t header, header_grad; /* top, bottom */ + twin_argb32_t on_bar; /* text/icons on the deep bars */ + twin_argb32_t navbar, navbar_grad; /* navigation tray */ + twin_argb32_t bg, bg_grad; + twin_argb32_t card; + twin_argb32_t group; /* grouped surface */ + twin_argb32_t text, text_sec; + twin_argb32_t separator; + twin_argb32_t call_green, call_red, missed_red; + twin_argb32_t sms_sent, sms_recv; + twin_argb32_t indicator; + twin_argb32_t on_primary; /* text/icons drawn on primary */ +} phone_theme_t; + +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); +/* Switch theme and repaint every view */ +void phone_theme_set(int idx); + +/* + * Colors (premultiplied ARGB; all theme colors are opaque) + */ +#define UI_RGB(rgb) ((twin_argb32_t) (0xff000000u | (rgb))) +#define UI_WHITE UI_RGB(0xffffff) +#define UI_BLACK UI_RGB(0x000000) +/* Mix a toward b by t/255 */ +twin_argb32_t ui_mix(twin_argb32_t a, twin_argb32_t b, int t); +/* Darken by pct percent (0..100) */ +twin_argb32_t ui_darken(twin_argb32_t c, int pct); +/* Scale an opaque color to alpha a (premultiplies) */ +twin_argb32_t ui_alpha(twin_argb32_t c, twin_a8_t a); + +/* + * Rectangles (half-open, client coordinates) + */ +typedef twin_rect_t ui_rect_t; + +static inline ui_rect_t ui_rect(twin_coord_t x, + twin_coord_t y, + twin_coord_t w, + twin_coord_t h) +{ + ui_rect_t r = {x, (twin_coord_t) (x + w), y, (twin_coord_t) (y + h)}; + return r; +} + +static inline twin_coord_t ui_w(ui_rect_t r) +{ + return r.right - r.left; +} + +static inline twin_coord_t ui_h(ui_rect_t r) +{ + return r.bottom - r.top; +} + +static inline bool ui_hit(ui_rect_t r, twin_coord_t x, twin_coord_t y) +{ + return r.left <= x && x < r.right && r.top <= y && y < r.bottom; +} + +static inline ui_rect_t ui_inset(ui_rect_t r, twin_coord_t d) +{ + ui_rect_t o = {(twin_coord_t) (r.left + d), (twin_coord_t) (r.right - d), + (twin_coord_t) (r.top + d), (twin_coord_t) (r.bottom - d)}; + return o; +} + +bool ui_intersects(ui_rect_t a, ui_rect_t b); + +static inline bool ui_empty(ui_rect_t r) +{ + return r.left >= r.right || r.top >= r.bottom; +} + +/* Smallest rectangle covering both (an empty operand is ignored) */ +static inline ui_rect_t ui_rect_union(ui_rect_t a, ui_rect_t b) +{ + if (ui_empty(a)) + return b; + if (ui_empty(b)) + return a; + ui_rect_t u = {a.left < b.left ? a.left : b.left, + a.right > b.right ? a.right : b.right, + a.top < b.top ? a.top : b.top, + a.bottom > b.bottom ? a.bottom : b.bottom}; + return u; +} + +static inline ui_rect_t ui_rect_intersect(ui_rect_t a, ui_rect_t b) +{ + ui_rect_t r = {a.left > b.left ? a.left : b.left, + a.right < b.right ? a.right : b.right, + a.top > b.top ? a.top : b.top, + a.bottom < b.bottom ? a.bottom : b.bottom}; + return r; +} + +/* @inner lies entirely inside @outer */ +static inline bool ui_rect_contains(ui_rect_t outer, ui_rect_t inner) +{ + return inner.left >= outer.left && inner.right <= outer.right && + inner.top >= outer.top && inner.bottom <= outer.bottom; +} + +/* + * Numbers + */ +static inline twin_fixed_t ui_fx(int v) +{ + return twin_int_to_fixed(v); +} + +/* from + (to - from) * t, rounded; t is 0..TWIN_FIXED_ONE (a tween value) */ +static inline int ui_lerp(int from, int to, twin_fixed_t t) +{ + return from + twin_fixed_to_int((to - from) * t + TWIN_FIXED_HALF); +} + +/* + * Typography: Montserrat for text, the icon font for TWIN_SYM_* symbols + */ +enum { + FONT_SM = 12, + FONT_NORMAL = 14, + FONT_HEADING = 16, + FONT_LARGE = 22, + FONT_CLOCK = 32, +}; + +typedef enum { + UI_LEFT, + UI_CENTER, + UI_RIGHT, +} ui_align_t; + +/* Draw @s with its baseline at @y */ +void ui_text(twin_pixmap_t *px, + int size, + twin_coord_t x, + twin_coord_t y, + ui_align_t align, + twin_argb32_t color, + const char *s); + +/* Draw @s vertically centered in @r, aligned horizontally (4 px padding) */ +void ui_text_in(twin_pixmap_t *px, + int size, + ui_rect_t r, + ui_align_t align, + twin_argb32_t color, + const char *s); + +/* Like ui_text_in() but truncates with an ellipsis to fit */ +void ui_text_fit(twin_pixmap_t *px, + int size, + ui_rect_t r, + ui_align_t align, + twin_argb32_t color, + const char *s); + +twin_coord_t ui_text_width(int size, const char *s); + +/* + * Word-wrap @s into lines no wider than @width, drawing each line at + * @line_h intervals with the first baseline at @y (px may be NULL to only + * measure). Returns the number of lines; *widest receives the widest line. + */ +int ui_text_wrap(twin_pixmap_t *px, + int size, + twin_coord_t x, + twin_coord_t y, + twin_coord_t width, + twin_coord_t line_h, + twin_argb32_t color, + const char *s, + twin_coord_t *widest); + +/* Baseline that vertically centers a line of @size text in [top, top+h) */ +twin_coord_t ui_baseline(int size, twin_coord_t top, twin_coord_t h); + +/* Draw a TWIN_SYM_* symbol centered at (cx, cy) */ +void ui_icon(twin_pixmap_t *px, + const char *sym, + int size, + twin_coord_t cx, + twin_coord_t cy, + twin_argb32_t color); + +/* + * Shapes (anti-aliased, cached by the coverage cache) + */ +void ui_fill(twin_pixmap_t *px, ui_rect_t r, twin_argb32_t color); +void ui_rrect(twin_pixmap_t *px, ui_rect_t r, int radius, twin_argb32_t color); +void ui_rrect_grad(twin_pixmap_t *px, + ui_rect_t r, + int radius, + twin_argb32_t top, + twin_argb32_t bottom); +void ui_rrect_stroke(twin_pixmap_t *px, + ui_rect_t r, + int radius, + int width, + twin_argb32_t color); +void ui_circle(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + twin_argb32_t color); +void ui_ring(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + int width, + twin_argb32_t color); +void ui_line(twin_pixmap_t *px, + twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1, + int width, + twin_argb32_t color); +/* Vertical gradient over a rectangle (no rounding) */ +void ui_vgrad(twin_pixmap_t *px, + ui_rect_t r, + twin_argb32_t top, + twin_argb32_t bottom); + +/* Scratch path for custom shapes; release it after painting */ +/* Create / free the kit's shared paths; phone_boot() calls these */ +bool ui_init(void); +void ui_fini(void); + +twin_path_t *ui_path(void); +void ui_path_done(twin_path_t *path); + +/* + * Views + */ +typedef enum { + UI_PRESS, /* finger down at (x, y) */ + UI_MOVE, /* finger moved before the drag threshold */ + UI_DRAG, /* dragging: (x, y) current, (dx, dy) since the press */ + UI_RELEASE, /* finger up after a drag; vx, vy in px/s */ + UI_TAP, /* finger up without dragging */ + UI_CANCEL, /* press abandoned (view lost the pointer) */ + UI_KEY, /* hardware key: a character, UI_KEY_ENTER or _BACKSPACE */ + UI_SHOW, /* the view became the top screen (focused) */ + UI_HIDE, /* the view stopped being the top screen */ + UI_RESIZE, /* the client area changed size (after the layout callback) */ +} ui_event_kind_t; + +typedef struct { + ui_event_kind_t kind; + twin_coord_t x, y; /* current position */ + twin_coord_t x0, y0; /* where the press started */ + twin_coord_t dx, dy; /* displacement since the press */ + int vx, vy; /* release velocity, px/s */ + twin_ucs4_t ucs4; /* UI_KEY */ +} ui_event_t; + +typedef struct ui_view ui_view_t; +typedef void (*ui_paint_t)(ui_view_t *view, twin_pixmap_t *px); +typedef bool (*ui_handler_t)(ui_view_t *view, const ui_event_t *event); +typedef void (*ui_destroy_t)(ui_view_t *view); +typedef void (*ui_layout_t)(ui_view_t *view); +typedef twin_time_t (*ui_timer_t)(ui_view_t *view); +typedef bool (*ui_tween_t)(ui_view_t *view, twin_fixed_t value); + +#define UI_VIEW_TIMERS 4 + +/* Kit internal: a timer or tween bound to a view (the callback's closure) */ +typedef struct { + ui_view_t *view; + void *handle; /* twin_timeout_t or twin_tween_t, NULL when free */ + union { + ui_timer_t timer; + ui_tween_t tween; + } fn; +} ui_slot_t; + +struct ui_view { + twin_window_t *window; + const char *title; + ui_paint_t paint; + ui_handler_t event; + ui_destroy_t destroy; /* free resources; the kit frees the struct */ + int app; /* index into the app registry, or -1 */ + + bool pressed; /* a finger is down on the view (read-only) */ + /* Kit internal: gesture tracking */ + bool dragging; + twin_coord_t px0, py0, plx, ply; + twin_time_t plt; + int pvx, pvy; + ui_layout_t layout; /* recomputes cached geometry; see set_layout */ + /* Kit internal: callbacks bound to the view's lifetime */ + ui_slot_t timers[UI_VIEW_TIMERS], tweens[UI_VIEW_TIMERS]; + bool *alive; /* cleared on destroy while a callback runs */ + struct ui_view *next_view; /* Kit internal: list of live views */ +}; + +/* + * Allocate a zeroed view of @size bytes (>= sizeof(ui_view_t)) as an app + * screen over the work area. Returns NULL on allocation failure. + */ +void *ui_view_new(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, + ui_paint_t paint, + ui_handler_t event, + ui_destroy_t destroy); + +/* Push the view on screen (slides in over the current one) */ +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); + +/* + * 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. + */ +void ui_view_set_layout(ui_view_t *view, ui_layout_t layout); + +/* The UI_KEY character if it is ASCII, else -1 (for keypad apps) */ +int ui_key_ascii(const ui_event_t *event); + +ui_rect_t ui_view_bounds(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); + +/* Region being repainted, in client coordinates */ +ui_rect_t ui_view_clip(ui_view_t *view); + +/* Schedule a repaint of @r / of everything */ +void ui_view_invalidate(ui_view_t *view, ui_rect_t r); +void ui_view_invalidate_all(ui_view_t *view); + +/* + * Move a pressed highlight: *slot becomes @next (-1 for none) and the old + * and new items, located by @rect, are repainted. Empty rects are skipped. + */ +void ui_view_set_pressed(ui_view_t *view, + int *slot, + int next, + ui_rect_t (*rect)(ui_view_t *view, int i)); + +/* Move pixels inside @r by (dx, dy) and repaint only the exposed band */ +void ui_view_scroll(ui_view_t *view, ui_rect_t r, int dx, int dy); + +/* View that owns @window, or NULL */ +ui_view_t *ui_view_of(twin_window_t *window); + +/* + * Timers and tweens bound to a view: cancelled automatically when the view + * is destroyed. A timer callback returns the delay to the next call in ms, + * or -1 to stop. Each view holds up to UI_VIEW_TIMERS of each. + */ +bool ui_view_every(ui_view_t *view, twin_time_t delay, ui_timer_t fn); +/* A timer running @fn is set on the view */ +bool ui_view_has_timer(ui_view_t *view, ui_timer_t fn); +void ui_view_stop_timers(ui_view_t *view); +void ui_view_stop_tweens(ui_view_t *view); +bool ui_view_tween(ui_view_t *view, + twin_time_t duration, + twin_ease_t ease, + ui_tween_t fn); + +/* + * Common widgets. All are stateless painters plus hit tests; the caller + * keeps state (pressed, value, ...) in its view struct. + */ + +/* + * Header bar on the deep surface: an optional symbol action at the start + * (UI_HEADER_SLOT px), the title centered, and optional short status text + * at the end. The title moves to the start only when centering it would + * collide with the end text. An edge without an action carries a dot + * texture, which never sits behind text. Returns the room the title may + * use, for a caller that draws its own title. + */ +#define UI_HEADER_SLOT 36 +#define UI_HEADER_TEXTURE 28 /* room kept clear of the title at a bare edge */ +ui_rect_t ui_header(twin_pixmap_t *px, + ui_rect_t r, + const char *lead, + const char *title, + const char *trailing); + +/* Section header (list group label) */ +void ui_section(twin_pixmap_t *px, ui_rect_t r, const char *label); + +/* Filled button with a text or symbol label */ +typedef enum { + UI_BTN_PRIMARY, /* primary fill, on_primary text */ + UI_BTN_CARD, /* card fill, text color */ + UI_BTN_GREEN, /* call green */ + UI_BTN_RED, /* call red */ + UI_BTN_FLAT, /* no fill until pressed */ +} ui_button_style_t; + +void ui_button(twin_pixmap_t *px, + ui_rect_t r, + int radius, + ui_button_style_t style, + bool pressed, + int size, + const char *label); + +/* List row: avatar/icon optional, title, optional subtitle and detail */ +void ui_list_row(twin_pixmap_t *px, + ui_rect_t r, + bool pressed, + const char *sym, + twin_argb32_t sym_color, + const char *title, + const char *subtitle, + const char *detail); + +/* Round avatar with an initial letter */ +void ui_avatar(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + char initial, + twin_argb32_t color); +twin_argb32_t ui_avatar_color(const char *name); + +/* Horizontal slider: value 0..1000 */ +void ui_slider(twin_pixmap_t *px, ui_rect_t r, int value, bool pressed); +int ui_slider_value_at(ui_rect_t r, twin_coord_t x); +/* Knob center for @value, the inverse of ui_slider_value_at() */ +twin_coord_t ui_slider_knob_x(ui_rect_t r, int value); + +/* On/off switch */ +void ui_switch(twin_pixmap_t *px, ui_rect_t r, bool on); + +/* Page indicator dots, current page highlighted */ +void ui_dots(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int count, + int current); + +/* Thin separator line along the bottom of @r */ +void ui_separator(twin_pixmap_t *px, ui_rect_t r); + +/* Narrow drawing to @r; returns the clip to hand to + * twin_pixmap_restore_clip() */ +twin_rect_t ui_clip_push(twin_pixmap_t *px, ui_rect_t r); + +/* + * Kinetic vertical scrolling. Keep a ui_scroll_t in the view, call + * ui_scroll_setup() when content height is known, feed every event to + * ui_scroll_event() first, and paint content shifted up by s->offset. + * Scrolling blits existing pixels and repaints only the exposed band. + */ +typedef struct { + ui_rect_t area; /* viewport, client coordinates */ + twin_coord_t content_h; /* total content height */ + twin_coord_t offset; /* current scroll offset, 0..max */ + twin_coord_t grab; /* offset at the start of a drag */ + int velocity; /* px/s during a fling */ + twin_time_t last; /* fling clock */ + bool flinging; + bool caught; /* this press stopped a fling: its tap is swallowed */ +} ui_scroll_t; + +void ui_scroll_setup(ui_scroll_t *s, ui_rect_t area, twin_coord_t content_h); +twin_coord_t ui_scroll_max(const ui_scroll_t *s); +/* Returns true when the event was a scroll gesture (do not treat as tap) */ +bool ui_scroll_event(ui_view_t *view, ui_scroll_t *s, const ui_event_t *e); +/* Scroll to an absolute offset (clamped), blitting what can be reused */ +void ui_scroll_to(ui_view_t *view, ui_scroll_t *s, twin_coord_t offset); +/* Thin indicator on the right edge while content overflows */ +void ui_scroll_bar(twin_pixmap_t *px, const ui_scroll_t *s); + +/* + * Single-line text field + */ +typedef struct { + char text[64]; + int len; + bool focused; +} ui_field_t; + +void ui_field(twin_pixmap_t *px, + ui_rect_t r, + const ui_field_t *f, + const char *placeholder); +void ui_field_set(ui_field_t *f, const char *s); +/* Apply a keyboard key; returns true if the text changed */ +bool ui_field_key(ui_field_t *f, int key); + +/* + * Soft keyboard, drawn inside the view along its bottom edge. + */ +enum { + UI_KEY_NONE = 0, + UI_KEY_BACKSPACE = 8, + UI_KEY_ENTER = 13, + UI_KEY_SHIFT = 0x100, + UI_KEY_MODE, /* letters <-> digits/symbols */ +}; + +typedef struct { + bool visible; + bool shift; + bool symbols; + int pressed; /* index of the pressed key, -1 when none */ + ui_rect_t dirty; /* keys whose look changed in the last event */ +} ui_kbd_t; + +#define UI_KBD_H 132 + +void ui_kbd_init(ui_kbd_t *k); +/* Area the keyboard occupies inside a view of the given height */ +ui_rect_t ui_kbd_rect(twin_coord_t view_h); +void ui_kbd_draw(twin_pixmap_t *px, const ui_kbd_t *k, ui_rect_t r); +/* + * Feed a view event; returns the key typed on UI_TAP (character code or + * UI_KEY_*), UI_KEY_NONE otherwise. Shift/mode keys are handled here. + * Afterwards k->dirty covers the keys whose look changed (empty if none): + * invalidate it rather than the whole keyboard. + */ +int ui_kbd_event(ui_kbd_t *k, ui_rect_t r, const ui_event_t *e); + +/* Rectangle of the key producing @code in the current layout */ +bool ui_kbd_find(const ui_kbd_t *k, ui_rect_t r, int code, ui_rect_t *out); + +/* + * Shell + */ + +/* Boot the phone on @screen: bars, home, lock screen. False when out of + * memory before anything was created. */ +bool phone_boot(twin_screen_t *screen); +void phone_shutdown(void); +twin_screen_t *phone_screen(void); + +/* Navigation */ +void phone_open(int app); /* launch an app from the registry */ +void phone_back(void); /* close the top view */ +void phone_home(void); /* close everything above home */ +int phone_depth(void); /* views above home (0 at home) */ +ui_view_t *phone_top(void); /* topmost view, NULL if none */ +void phone_lock(void); /* show the lock screen */ +bool phone_locked(void); + +/* Home screen wallpaper */ +int phone_wallpaper(void); +int phone_wallpaper_count(void); +const char *phone_wallpaper_name(int idx); +void phone_set_wallpaper(int idx); +/* Paint wallpaper @idx into @r; returns the label color that reads on it */ +twin_argb32_t phone_paint_wallpaper(twin_pixmap_t *px, ui_rect_t r, int idx); + +/* Wall clock; tests install a fixed source */ +typedef struct { + int hour, min, sec; + int wday; /* 0 = Sunday */ + int mday; /* 1..31 */ + int mon; /* 0 = January */ +} phone_clock_t; + +void phone_clock(phone_clock_t *c); +void phone_set_clock_source(void (*source)(phone_clock_t *c)); +void phone_format_time(char *buf, size_t len); /* "12:03" */ +void phone_format_date(char *buf, size_t len); /* "Saturday, May 9" */ + +/* App registry */ +typedef void (*phone_glyph_t)(twin_pixmap_t *px, + ui_rect_t plate, + twin_argb32_t fg); + +typedef struct { + const char *name; + const char *sym; /* TWIN_SYM_* or NULL */ + phone_glyph_t glyph; /* procedural icon when sym is NULL */ + twin_argb32_t accent; /* icon plate color */ + 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; +} phone_app_t; + +int phone_app_count(void); +const phone_app_t *phone_app_at(int idx); +int phone_app_find(const char *name); + +/* Draw an app's home-screen icon (plate + glyph) into @plate */ +void phone_app_icon(twin_pixmap_t *px, int app, ui_rect_t plate, bool pressed); + +/* + * Apps. Each opens its first screen as a new view. + */ +void app_dialer_open(void); +void app_contacts_open(void); +void app_sms_open(void); +void app_calc_open(void); +void app_music_open(void); +void app_photos_open(void); +void app_camera_open(void); +void app_settings_open(void); +void app_calllog_open(void); +void app_snake_open(void); +void app_pong_open(void); +void app_tetris_open(void); + +/* Home and lock screens (shell.c) */ +ui_view_t *home_create(void); +ui_view_t *lock_create(void); + +/* Dial a number from another app (contacts, call log) */ +void app_dialer_call(const char *name, const char *number); + +#endif /* _APPS_PHONE_H_ */ diff --git a/apps/cellphone/photos.c b/apps/cellphone/photos.c new file mode 100644 index 00000000..af385b93 --- /dev/null +++ b/apps/cellphone/photos.c @@ -0,0 +1,757 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Photos: a 3x2 thumbnail grid and a swipeable single-photo viewer. + * + * Memory is bounded on purpose: the six 74x74 thumbnails are decoded once + * into a gallery shared (by reference count) between the grid and the + * viewer, and the viewer holds at most one full-size picture. That one is + * decoded only after the push animation has settled, so opening a photo + * never stalls the transition; until then the thumbnail is shown scaled up. + * + * Without a JPEG loader, or when a file is missing, every picture is + * replaced by procedural art rendered into a pixmap of the same size, so + * the rest of the code only ever deals with pixmaps. + */ + +#include + +#include "phone.h" + +#define N_PHOTOS 6 +#define COLS 3 +#define THUMB 74 /* thumbnail cell (and the size of the thumb JPEGs) */ +#define GAP 4 +#define GRID_LEFT ((PHONE_W - COLS * THUMB - (COLS - 1) * GAP) / 2) +#define GRID_TOP (PHONE_HEADER_H + PHONE_SECTION_H + 4) +#define CAPTION_H 18 +#define THUMB_RADIUS 6 +#define CAPTION_FONT 11 /* fits "Constitution" in a thumbnail */ + +#define BAR_H 26 /* viewer chrome bars */ +#define DEFER_MS 260 /* push animation length plus a little slack */ +#define SETTLE_MS 220 /* swipe settle animation */ +#define SWIPE_FLING 300 /* px/s that flips a photo regardless of distance */ +#define CHROME UI_RGB(0x1a1a1a) + +typedef struct { + const char *title; + const char *stem; +} photo_t; + +static const photo_t photos[N_PHOTOS] = { + {"Portrait", "bob_kerrey_portrait"}, + {"Landscape", "landscape_1677"}, + {"Constitution", "us_constitution"}, + {"Solar Loops", "solar_magnetic_loops"}, + {"Apollo 16", "apollo16_jump"}, + {"Cupola", "iss_window"}, +}; + + +/* + * Pixmap helpers + */ + + +/* + * Draw @src into @r, scaled to cover it (centered, cropping the overflow). + * The core's transformed composite maps destination pixels relative to the + * clipped rectangle, so clip here first and fold the offset into the + * matrix; otherwise a partial repaint would sample the wrong texels. + */ +static void blit_cover(twin_pixmap_t *dst, twin_pixmap_t *src, ui_rect_t r) +{ + twin_operand_t s = {.source_kind = TWIN_PIXMAP, .u.pixmap = src}; + twin_coord_t w = ui_w(r), h = ui_h(r); + if (w <= 0 || h <= 0) + return; + twin_coord_t sw = twin_pixmap_width(src), sh = twin_pixmap_height(src); + if (sw == w && sh == h) { + twin_composite(dst, r.left, r.top, &s, 0, 0, NULL, 0, 0, TWIN_SOURCE, w, + h); + return; + } + ui_rect_t c = ui_rect_intersect(r, twin_pixmap_get_clip(dst)); + if (c.left >= c.right || c.top >= c.bottom) + return; + /* Destination-to-source scale; the last texel row/column is excluded so + * bilinear filtering never blends with the transparent outside */ + twin_fixed_t sx = twin_fixed_div(ui_fx(sw - 1), ui_fx(w)); + twin_fixed_t sy = twin_fixed_div(ui_fx(sh - 1), ui_fx(h)); + twin_fixed_t k = sx < sy ? sx : sy; + twin_fixed_t x0 = (ui_fx(sw - 1) - k * w) / 2; + twin_fixed_t y0 = (ui_fx(sh - 1) - k * h) / 2; + twin_matrix_t m = { + {{k, 0}, + {0, k}, + {x0 + k * (c.left - r.left), y0 + k * (c.top - r.top)}}}; + twin_pixmap_set_transform(src, &m); + twin_composite(dst, c.left, c.top, &s, 0, 0, NULL, 0, 0, TWIN_SOURCE, + ui_w(c), ui_h(c)); + twin_pixmap_set_transform(src, NULL); +} + +/* + * Procedural stand-ins, one per photo, drawn in permille coordinates of + * @r so the same art serves as a thumbnail and as a full-size picture. + */ +#define AX(v) (r.left + ui_w(r) * (v) / 1000) +#define AY(v) (r.top + ui_h(r) * (v) / 1000) +#define AS(v) (unit * (v) / 1000) + +static void ellipse(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int rx, + int ry, + twin_argb32_t c) +{ + twin_path_t *p = ui_path(); + twin_path_ellipse(p, ui_fx(cx), ui_fx(cy), ui_fx(rx), ui_fx(ry)); + twin_paint_path(px, c, p); + ui_path_done(p); +} + +static void arc_stroke(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int rx, + int ry, + int width, + twin_argb32_t c) +{ + twin_path_t *p = ui_path(); + twin_path_move(p, ui_fx(cx - rx), ui_fx(cy)); + twin_path_arc(p, ui_fx(cx), ui_fx(cy), ui_fx(rx), ui_fx(ry), TWIN_ANGLE_180, + TWIN_ANGLE_180); + twin_paint_stroke(px, c, p, + ui_fx(width) > 0 ? ui_fx(width) : TWIN_FIXED_HALF); + ui_path_done(p); +} + +static void paint_art(twin_pixmap_t *px, ui_rect_t r, int idx) +{ + int w = ui_w(r), h = ui_h(r); + int unit = w < h ? w : h; + switch (idx) { + case 0: /* Portrait: studio backdrop, suit and tie, head */ + ui_vgrad(px, r, UI_RGB(0x6f86a6), UI_RGB(0x2b3a52)); + ui_fill(px, + ui_rect(AX(500) - AS(55), AY(480), 2 * AS(55), h * 15 / 100), + UI_RGB(0xc99a7c)); + ellipse(px, AX(500), AY(1020), w * 50 / 100, h * 42 / 100, + UI_RGB(0x1d2430)); + ellipse(px, AX(500), AY(640), AS(80), AS(80), UI_RGB(0xeeeeee)); + ellipse(px, AX(500), AY(720), AS(26), AS(120), UI_RGB(0x7a1f2b)); + ellipse(px, AX(500), AY(300), AS(150), AS(110), UI_RGB(0x3b2a20)); + ellipse(px, AX(500), AY(390), AS(125), AS(160), UI_RGB(0xd9a989)); + break; + case 1: /* Landscape (1677): warm sky, hills, a tree */ + ui_vgrad(px, r, UI_RGB(0xb9c4c2), UI_RGB(0xead8a8)); + ellipse(px, AX(250), AY(760), w * 60 / 100, h * 22 / 100, + UI_RGB(0x8d8c62)); + ellipse(px, AX(850), AY(820), w * 55 / 100, h * 24 / 100, + UI_RGB(0x6b6a45)); + ui_fill(px, ui_rect(r.left, AY(850), w, h - h * 850 / 1000), + UI_RGB(0x4a4428)); + ui_fill(px, ui_rect(AX(300) - AS(20), AY(470), AS(40), h * 40 / 100), + UI_RGB(0x3a2c1c)); + ellipse(px, AX(300), AY(400), AS(210), AS(170), UI_RGB(0x3e4a27)); + ellipse(px, AX(220), AY(470), AS(130), AS(110), UI_RGB(0x4a5730)); + break; + case 2: { /* Constitution: parchment with a heading and columns */ + ui_vgrad(px, r, UI_RGB(0xefe2c0), UI_RGB(0xd2bc88)); + int size = unit >= 160 ? FONT_LARGE : 0; + int top = 180; + if (size) { + ui_text(px, size, AX(500), AY(170), UI_CENTER, UI_RGB(0x4a3b22), + "We the People"); + top = 240; + } else { + ui_fill(px, ui_rect(AX(150), AY(120), w * 70 / 100, AS(50)), + UI_RGB(0x5a4a2c)); + } + twin_coord_t lh = h / (unit >= 160 ? 40 : 22); + for (int col = 0; col < 3; col++) { + twin_coord_t x = AX(80 + col * 290); + for (twin_coord_t y = AY(top); y < AY(920); y += lh * 2) { + int len = 200 + (int) ((y * 7 + col * 13) % 60); + ui_fill(px, ui_rect(x, y, w * len / 1000, lh > 1 ? lh / 2 : 1), + UI_RGB(0x8a7650)); + } + } + break; + } + case 3: /* Solar loops: dark corona over a glowing limb */ + ui_vgrad(px, r, UI_RGB(0x140400), UI_RGB(0x4a1500)); + for (int i = 0; i < 4; i++) + arc_stroke(px, AX(280 + i * 140), AY(700), AS(160 + i * 40), + AS(300 + i * 60), unit / 50, + ui_alpha(UI_RGB(0xffb347), 200 - i * 30)); + ellipse(px, AX(500), AY(1350), w * 95 / 100, h * 55 / 100, + UI_RGB(0xe8590c)); + ellipse(px, AX(500), AY(1400), w * 85 / 100, h * 55 / 100, + UI_RGB(0xff8c2a)); + break; + case 4: /* Apollo 16: black sky, regolith, astronaut and flag */ + ui_fill(px, r, UI_RGB(0x05070a)); + ui_vgrad(px, ui_rect(r.left, AY(640), w, h - h * 640 / 1000), + UI_RGB(0x9a9a94), UI_RGB(0x55554f)); + ui_fill(px, ui_rect(AX(250), AY(250), AS(14) + 1, h * 45 / 100), + UI_RGB(0xb0b0b0)); + ui_fill(px, ui_rect(AX(250), AY(250), AS(220), AS(140)), + UI_RGB(0xb22234)); + ui_fill(px, ui_rect(AX(250), AY(250), AS(100), AS(80)), + UI_RGB(0x3c3b6e)); + ui_rrect(px, ui_rect(AX(590), AY(400), AS(200), AS(260)), AS(60), + UI_RGB(0xf2f2ee)); + ellipse(px, AX(590) + AS(100), AY(400) - AS(40), AS(95), AS(95), + UI_RGB(0xf2f2ee)); + ellipse(px, AX(590) + AS(100), AY(400) - AS(35), AS(65), AS(55), + UI_RGB(0xc8901a)); + ellipse(px, AX(640), AY(1000) - AS(20), AS(240), AS(30), + ui_alpha(UI_BLACK, 90)); + break; + default: /* Cupola: earth below, window frame around */ + ui_fill(px, r, UI_RGB(0x0b0f1a)); + ellipse(px, AX(500), AY(1250), w * 90 / 100, h * 62 / 100, + UI_RGB(0x8ec5f0)); + ellipse(px, AX(500), AY(1270), w * 88 / 100, h * 62 / 100, + UI_RGB(0x2a6fb5)); + ellipse(px, AX(380), AY(820), AS(200), AS(50), ui_alpha(UI_WHITE, 170)); + ellipse(px, AX(680), AY(900), AS(160), AS(40), ui_alpha(UI_WHITE, 140)); + ui_rrect_stroke(px, ui_inset(r, AS(60)), AS(90), AS(70) + 1, + UI_RGB(0x8a8f99)); + break; + } +} + +#undef AX +#undef AY +#undef AS + +/* Render the procedural stand-in for @idx at w x h */ +static twin_pixmap_t *make_art(int idx, twin_coord_t w, twin_coord_t h) +{ + twin_pixmap_t *pix = twin_pixmap_create(TWIN_ARGB32, w, h); + if (pix) + paint_art(pix, ui_rect(0, 0, w, h), idx); + return pix; +} + +/* + * Decode a photo derivative. Paths are relative to the working directory; + * the WebAssembly build mounts the assets at /assets. + */ +static twin_pixmap_t *load_jpeg(const char *dir, const char *stem) +{ +#if defined(CONFIG_LOADER_JPEG) + static const char *const roots[] = {"assets/photos", "/assets/photos"}; + char path[96]; + for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); i++) { + snprintf(path, sizeof(path), "%s/%s/%s_%s.jpg", roots[i], dir, stem, + dir[0] == 't' ? "thumb" : "view"); + twin_pixmap_t *pix = twin_pixmap_from_file(path); + if (pix) + return pix; + } +#else + (void) dir; + (void) stem; +#endif + return NULL; +} + +/* Photo @idx at exactly w x h: decoded and resized, or procedural */ +static twin_pixmap_t *load_photo(int idx, + const char *dir, + twin_coord_t w, + twin_coord_t h) +{ + twin_pixmap_t *pix = load_jpeg(dir, photos[idx].stem); + if (pix && (twin_pixmap_width(pix) != w || twin_pixmap_height(pix) != h || + twin_pixmap_animation(pix))) { + /* Normalize once here so painting never scales a full picture */ + twin_pixmap_t *fit = twin_pixmap_create(TWIN_ARGB32, w, h); + if (fit) + blit_cover(fit, pix, ui_rect(0, 0, w, h)); + twin_pixmap_destroy(pix); + pix = fit; + } + return pix ? pix : make_art(idx, w, h); +} + +/* + * Gallery: thumbnails and the rounded-corner mask, shared by the grid and + * the viewer and freed when the last of them closes. + */ +static struct { + int refs; + twin_pixmap_t *thumb[N_PHOTOS]; + twin_pixmap_t *mask; /* A8 rounded rectangle, THUMB x THUMB */ +} gallery; + +/* @pix as RGB16: photos are opaque, so the copy kept for the gallery's + * lifetime takes half the memory */ +static twin_pixmap_t *to_rgb16(twin_pixmap_t *pix) +{ + if (!pix) + return NULL; + twin_coord_t w = twin_pixmap_width(pix), h = twin_pixmap_height(pix); + twin_pixmap_t *out = twin_pixmap_create(TWIN_RGB16, w, h); + if (out) { + twin_operand_t src = {.source_kind = TWIN_PIXMAP, .u.pixmap = pix}; + twin_composite(out, 0, 0, &src, 0, 0, NULL, 0, 0, TWIN_SOURCE, w, h); + twin_pixmap_destroy(pix); + return out; + } + return pix; +} + +static void gallery_acquire(void) +{ + if (gallery.refs++) + return; + for (int i = 0; i < N_PHOTOS; i++) + gallery.thumb[i] = to_rgb16(load_photo(i, "thumbs", THUMB, THUMB)); + gallery.mask = twin_pixmap_create(TWIN_A8, THUMB, THUMB); + if (gallery.mask) { + twin_fill(gallery.mask, 0, TWIN_SOURCE, 0, 0, THUMB, THUMB); + twin_path_t *p = ui_path(); + twin_path_rounded_rectangle(p, 0, 0, ui_fx(THUMB), ui_fx(THUMB), + ui_fx(THUMB_RADIUS), ui_fx(THUMB_RADIUS)); + twin_paint_path(gallery.mask, UI_BLACK, p); + ui_path_done(p); + } +} + +static void gallery_release(void) +{ + if (--gallery.refs) + return; + for (int i = 0; i < N_PHOTOS; i++) { + if (gallery.thumb[i]) + twin_pixmap_destroy(gallery.thumb[i]); + gallery.thumb[i] = NULL; + } + if (gallery.mask) + twin_pixmap_destroy(gallery.mask); + gallery.mask = NULL; +} + +/* Fill the thumbnail-shaped area at (x, y) with @c (rounded if possible) */ +static void masked_fill(twin_pixmap_t *px, + twin_coord_t x, + twin_coord_t y, + twin_coord_t my, + twin_coord_t h, + twin_argb32_t c) +{ + twin_operand_t s = {.source_kind = TWIN_SOLID, .u.argb = c}; + twin_operand_t m = {.source_kind = TWIN_PIXMAP, .u.pixmap = gallery.mask}; + twin_composite(px, x, y, &s, 0, 0, gallery.mask ? &m : NULL, 0, my, + TWIN_OVER, THUMB, h); +} + +/* + * Grid + */ +typedef struct { + ui_view_t view; + int pressed; /* thumbnail under the finger, -1 when none */ +} grid_t; + +static void viewer_open(int idx); + +static ui_rect_t thumb_rect(int i) +{ + return ui_rect(GRID_LEFT + (i % COLS) * (THUMB + GAP), + GRID_TOP + (i / COLS) * (THUMB + GAP), THUMB, THUMB); +} + +static int thumb_at(twin_coord_t x, twin_coord_t y) +{ + for (int i = 0; i < N_PHOTOS; i++) + if (ui_hit(thumb_rect(i), x, y)) + return i; + return -1; +} + +static void grid_paint(ui_view_t *v, twin_pixmap_t *px) +{ + grid_t *g = (grid_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + ui_rect_t head = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); + ui_rect_t sect = ui_rect(0, PHONE_HEADER_H, PHONE_W, PHONE_SECTION_H); + + ui_fill(px, ui_view_bounds(v), th->bg); + if (ui_intersects(clip, head)) + ui_header(px, head, NULL, "Photos", "6 items"); + if (ui_intersects(clip, sect)) + ui_section(px, sect, "Recents"); + + for (int i = 0; i < N_PHOTOS; i++) { + ui_rect_t r = thumb_rect(i); + if (!ui_intersects(clip, r)) + continue; + twin_pixmap_t *t = gallery.thumb[i]; + if (t) { + twin_operand_t s = {.source_kind = TWIN_PIXMAP, .u.pixmap = t}; + twin_operand_t m = {.source_kind = TWIN_PIXMAP, + .u.pixmap = gallery.mask}; + twin_composite(px, r.left, r.top, &s, 0, 0, + gallery.mask ? &m : NULL, 0, 0, TWIN_OVER, THUMB, + THUMB); + } else { + ui_rrect(px, r, THUMB_RADIUS, th->card); + } + /* Caption on a translucent shade along the bottom edge */ + masked_fill(px, r.left, r.bottom - CAPTION_H, THUMB - CAPTION_H, + CAPTION_H, ui_alpha(UI_BLACK, 120)); + ui_text_fit( + px, CAPTION_FONT, + ui_rect(r.left + 1, r.bottom - CAPTION_H, THUMB - 2, CAPTION_H), + UI_LEFT, UI_WHITE, photos[i].title); + if (i == g->pressed) + masked_fill(px, r.left, r.top, 0, THUMB, ui_alpha(UI_BLACK, 80)); + } + + ui_rect_t foot = + ui_rect(0, thumb_rect(N_PHOTOS - 1).bottom + 8, PHONE_W, 24); + if (ui_intersects(clip, foot)) + ui_text_in(px, FONT_SM, foot, UI_CENTER, th->text_sec, + "Public domain photos"); +} + +static ui_rect_t grid_thumb(ui_view_t *v, int i) +{ + (void) v; + return thumb_rect(i); +} + +static void grid_set_pressed(grid_t *g, int i) +{ + ui_view_set_pressed(&g->view, &g->pressed, i, grid_thumb); +} + +static bool grid_event(ui_view_t *v, const ui_event_t *e) +{ + grid_t *g = (grid_t *) v; + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + grid_set_pressed(g, thumb_at(e->x, e->y)); + return true; + case UI_TAP: { + int i = thumb_at(e->x, e->y); + grid_set_pressed(g, -1); + if (i >= 0) + viewer_open(i); + return true; + } + default: + grid_set_pressed(g, -1); + return false; + } +} + +static void grid_destroy(ui_view_t *v) +{ + (void) v; + gallery_release(); +} + +void app_photos_open(void) +{ + grid_t *g = + ui_view_new(sizeof(*g), "Photos", grid_paint, grid_event, grid_destroy); + if (!g) + return; + g->pressed = -1; + gallery_acquire(); + ui_view_show(&g->view); +} + +/* + * Viewer + */ +typedef struct { + ui_view_t view; + int idx; /* settled photo */ + twin_coord_t pan; /* horizontal offset: > 0 shows the next photo */ + twin_coord_t from, to; /* settle animation */ + bool settling; + bool chrome; /* title and dot bars visible */ + twin_pixmap_t *hires; /* full-size picture of hires_idx, or NULL */ + int hires_idx; +} viewer_t; + +/* Full-size pictures alive across all viewers (test hook) */ +static int hires_alive; + +static ui_rect_t top_bar(void) +{ + return ui_rect(0, 0, PHONE_W, BAR_H); +} + +static ui_rect_t bottom_bar(const viewer_t *vw) +{ + return ui_rect(0, ui_view_height(&vw->view) - BAR_H, PHONE_W, BAR_H); +} + +/* Area whose pixels can be blitted while panning (chrome stays put) */ +static ui_rect_t pan_area(viewer_t *vw) +{ + twin_coord_t h = ui_view_height(&vw->view); + if (!vw->chrome) + return ui_rect(0, 0, PHONE_W, h); + return ui_rect(0, BAR_H, PHONE_W, h - 2 * BAR_H); +} + +static void drop_hires(viewer_t *vw) +{ + if (!vw->hires) + return; + twin_pixmap_destroy(vw->hires); + vw->hires = NULL; + vw->hires_idx = -1; + hires_alive--; +} + +/* Photo shown under the dots while panning: the one covering the center */ +static int shown_index(viewer_t *vw) +{ + int i = vw->idx + (vw->pan >= 0 ? (vw->pan + PHONE_W / 2) / PHONE_W + : -((-vw->pan + PHONE_W / 2) / PHONE_W)); + return i < 0 ? 0 : i >= N_PHOTOS ? N_PHOTOS - 1 : i; +} + +static void paint_dots(twin_pixmap_t *px, twin_coord_t cy, int current) +{ + twin_coord_t x = PHONE_W / 2 - (N_PHOTOS * 10 + 6) / 2; + for (int i = 0; i < N_PHOTOS; i++) { + if (i == current) { + ui_rrect(px, ui_rect(x, cy - 3, 14, 6), 3, UI_WHITE); + x += 18; + } else { + ui_circle(px, x + 3, cy, 3, ui_alpha(UI_WHITE, 110)); + x += 10; + } + } +} + +static void viewer_paint(ui_view_t *v, twin_pixmap_t *px) +{ + viewer_t *vw = (viewer_t *) v; + ui_rect_t clip = ui_view_clip(v); + + ui_fill(px, ui_view_bounds(v), UI_BLACK); + for (int k = -1; k <= 1; k++) { + int i = vw->idx + k; + ui_rect_t r = + ui_rect(k * PHONE_W - vw->pan, 0, PHONE_W, ui_view_height(v)); + if (i < 0 || i >= N_PHOTOS || !ui_intersects(clip, r)) + continue; + if (vw->hires && vw->hires_idx == i) + blit_cover(px, vw->hires, r); + else if (gallery.thumb[i]) + blit_cover(px, gallery.thumb[i], r); + } + if (!vw->chrome) + return; + + int shown = shown_index(vw); + if (ui_intersects(clip, top_bar())) { + char count[16]; + snprintf(count, sizeof(count), "%d / %d", shown + 1, N_PHOTOS); + ui_fill(px, top_bar(), ui_alpha(CHROME, 200)); + ui_text_in(px, FONT_SM, ui_rect(4, 0, PHONE_W - 60, BAR_H), UI_LEFT, + UI_WHITE, photos[shown].title); + ui_text_in(px, FONT_SM, ui_rect(PHONE_W - 60, 0, 56, BAR_H), UI_RIGHT, + UI_WHITE, count); + } + if (ui_intersects(clip, bottom_bar(vw))) { + ui_fill(px, bottom_bar(vw), ui_alpha(CHROME, 200)); + paint_dots(px, ui_view_height(v) - BAR_H / 2, shown); + } +} + +/* Deferred decode of the full-size picture for the settled photo */ +static twin_time_t viewer_load(ui_view_t *v) +{ + viewer_t *vw = (viewer_t *) v; + /* Never decode under a moving finger or animation: try again shortly */ + if (v->pressed || vw->settling) + return 60; + if (vw->hires_idx == vw->idx) + return -1; + drop_hires(vw); + vw->hires = load_photo(vw->idx, "view", PHONE_W, ui_view_height(v)); + if (vw->hires) { + vw->hires_idx = vw->idx; + hires_alive++; + ui_view_invalidate(v, pan_area(vw)); + if (vw->chrome) { + ui_view_invalidate(v, top_bar()); + ui_view_invalidate(v, bottom_bar(vw)); + } + } + return -1; +} + +static void arm_load(viewer_t *vw) +{ + /* The viewer's only timer, so stopping all of them re-arms it */ + ui_view_stop_timers(&vw->view); + ui_view_every(&vw->view, DEFER_MS, viewer_load); +} + +static void pan_to(viewer_t *vw, twin_coord_t pan) +{ + twin_coord_t d = pan - vw->pan; + if (!d) + return; + vw->pan = pan; + ui_view_scroll(&vw->view, pan_area(vw), -d, 0); + if (vw->chrome) { + /* Bars sit over the moving photo, so they repaint every step */ + ui_view_invalidate(&vw->view, top_bar()); + ui_view_invalidate(&vw->view, bottom_bar(vw)); + } +} + +static bool settle_step(ui_view_t *v, twin_fixed_t value) +{ + viewer_t *vw = (viewer_t *) v; + pan_to(vw, ui_lerp(vw->from, vw->to, value)); + if (value < TWIN_FIXED_ONE) + return true; + vw->settling = false; + if (vw->to) { + /* Same pixels, new reference photo: no repaint needed */ + vw->idx += vw->to > 0 ? 1 : -1; + vw->pan = 0; + if (vw->hires_idx != vw->idx) + drop_hires(vw); + arm_load(vw); + } + return true; +} + +static bool viewer_event(ui_view_t *v, const ui_event_t *e) +{ + viewer_t *vw = (viewer_t *) v; + if (e->kind == UI_RESIZE) { + /* The full-size picture was decoded for the old height */ + drop_hires(vw); + arm_load(vw); + return true; + } + if (vw->settling) + return true; /* let the settle finish before the next gesture */ + switch (e->kind) { + case UI_DRAG: { + twin_coord_t pan = -e->dx; + /* Rubber band past the first and last photo */ + if ((pan < 0 && vw->idx == 0) || (pan > 0 && vw->idx == N_PHOTOS - 1)) + pan /= 3; + if (pan > PHONE_W) + pan = PHONE_W; + else if (pan < -PHONE_W) + pan = -PHONE_W; + pan_to(vw, pan); + return true; + } + case UI_RELEASE: + case UI_CANCEL: { + twin_coord_t to = 0; + if (e->kind == UI_RELEASE) { + if ((e->dx < -PHONE_W / 4 || e->vx < -SWIPE_FLING) && + vw->idx < N_PHOTOS - 1) + to = PHONE_W; + else if ((e->dx > PHONE_W / 4 || e->vx > SWIPE_FLING) && + vw->idx > 0) + to = -PHONE_W; + } + vw->from = vw->pan; + vw->to = to; + if (vw->from != vw->to) { + vw->settling = true; + if (!ui_view_tween(v, SETTLE_MS, TwinEaseOutCubic, settle_step)) { + vw->settling = false; + settle_step(v, TWIN_FIXED_ONE); + } + } + return true; + } + case UI_TAP: + /* Tap toggles the chrome for an unobstructed look */ + vw->chrome = !vw->chrome; + ui_view_invalidate_all(v); + return true; + default: + return e->kind != UI_KEY; + } +} + +static void viewer_destroy(ui_view_t *v) +{ + drop_hires((viewer_t *) v); + gallery_release(); +} + +static void viewer_open(int idx) +{ + viewer_t *vw = ui_view_new(sizeof(*vw), "Photo", viewer_paint, viewer_event, + viewer_destroy); + if (!vw) + return; + vw->idx = idx; + vw->chrome = true; + vw->hires_idx = -1; + gallery_acquire(); + ui_view_show(&vw->view); + arm_load(vw); +} + +/* + * Test hooks + */ +static viewer_t *top_viewer(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == viewer_paint ? (viewer_t *) v : NULL; +} + +/* Photo index in the viewer on top, -1 when the viewer is not on top */ +int app_photos_test_index(void) +{ + viewer_t *vw = top_viewer(); + return vw ? vw->idx : -1; +} + +/* Whether the viewer on top shows the full-size picture */ +bool app_photos_test_hires(void) +{ + viewer_t *vw = top_viewer(); + return vw && vw->hires && vw->hires_idx == vw->idx; +} + +/* Full-size pictures currently alive */ +int app_photos_test_hires_count(void) +{ + return hires_alive; +} + +/* Whether the thumbnail grid is on top */ +bool app_photos_test_grid_open(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == grid_paint; +} diff --git a/apps/cellphone/pong.c b/apps/cellphone/pong.c new file mode 100644 index 00000000..9bdc566f --- /dev/null +++ b/apps/cellphone/pong.c @@ -0,0 +1,209 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* Arcade: Pong (see games.c) */ + +#include + +#include "games-internal.h" + +/* + * Pong: the ball moves in subpixels and speeds up on every hit; paddles + * chase the predicted crossing point with a capped speed and a random + * aiming error, so fast rallies eventually end in a miss. + */ + +static ui_rect_t pong_field(void) +{ + return ui_inset(game_panel_rect(), PG_INSET); +} + +static twin_coord_t pong_paddle_x(int side) +{ + ui_rect_t f = pong_field(); + return side ? f.right - 4 - PG_PW : f.left + 4; +} + +static ui_rect_t pong_paddle_rect(const pong_t *p, int side) +{ + return ui_rect(pong_paddle_x(side), p->py[side] / PG_S, PG_PW, PG_PH); +} + +static ui_rect_t pong_ball_rect(const pong_t *p) +{ + return ui_rect(p->bx / PG_S, p->by / PG_S, PG_BALL, PG_BALL); +} + +static void pong_center_ball(pong_t *p) +{ + ui_rect_t f = pong_field(); + p->bx = ((f.left + f.right) / 2 - PG_BALL / 2) * PG_S; + p->by = ((f.top + f.bottom) / 2 - PG_BALL / 2) * PG_S; + p->vx = p->vy = 0; + p->serve = PG_SERVE_TICKS; +} + +static void pong_init(game_t *g) +{ + pong_t *p = &g->u.pong; + ui_rect_t f = pong_field(); + memset(p, 0, sizeof(*p)); + p->py[0] = p->py[1] = ((f.top + f.bottom) / 2 - PG_PH / 2) * PG_S; + p->serve_right = game_rnd(g) & 1; + pong_center_ball(p); +} + +/* Ball center y when it reaches the face of paddle @side */ +static int32_t pong_predict(const pong_t *p, int side) +{ + ui_rect_t f = pong_field(); + int32_t face = side ? (pong_paddle_x(1) - PG_BALL) * PG_S + : (pong_paddle_x(0) + PG_PW) * PG_S; + int32_t vx = p->vx < 0 ? -p->vx : p->vx; + int32_t dx = face - p->bx; + if (dx < 0) + dx = -dx; + int32_t t = vx ? dx / vx : 0; + int32_t range = (ui_h(f) - PG_BALL) * PG_S; + int32_t y = p->by - f.top * PG_S + p->vy * t; + /* Fold the straight-line path back into the field (wall bounces) */ + y %= 2 * range; + if (y < 0) + y += 2 * range; + if (y > range) + y = 2 * range - y; + return y + f.top * PG_S + PG_BALL * PG_S / 2; +} + +static void pong_paddle(game_t *g, int side) +{ + pong_t *p = &g->u.pong; + ui_rect_t f = pong_field(); + bool coming = p->serve == 0 && (side ? p->vx > 0 : p->vx < 0); + int32_t target = coming ? pong_predict(p, side) + p->aim[side] * PG_S + : (f.top + f.bottom) / 2 * PG_S; + int32_t want = target - PG_PH * PG_S / 2; + int32_t speed = coming ? PG_PSPEED : PG_PSPEED / 2; + int32_t d = want - p->py[side]; + if (d > speed) + d = speed; + if (d < -speed) + d = -speed; + int32_t y = p->py[side] + d; + if (y < f.top * PG_S) + y = f.top * PG_S; + if (y > (f.bottom - PG_PH) * PG_S) + y = (f.bottom - PG_PH) * PG_S; + if (y == p->py[side]) + return; + ui_rect_t old = pong_paddle_rect(p, side); + p->py[side] = y; + game_mark(g, ui_rect_union(old, pong_paddle_rect(p, side))); +} + +/* Bounce off paddle @side if the ball reached it this step */ +static bool pong_hit(game_t *g, int side) +{ + pong_t *p = &g->u.pong; + twin_coord_t px = pong_paddle_x(side); + if (side ? p->vx <= 0 : p->vx >= 0) + return false; + int32_t face = side ? (px - PG_BALL) * PG_S : (px + PG_PW) * PG_S; + if (side ? p->bx < face : p->bx > face) + return false; + /* Already behind the paddle: too late */ + if (side ? p->bx > (px + PG_PW) * PG_S : p->bx + PG_BALL * PG_S < px * PG_S) + return false; + int32_t ball_c = p->by + PG_BALL * PG_S / 2; + int32_t pad_c = p->py[side] + PG_PH * PG_S / 2; + int32_t off = (ball_c - pad_c) / PG_S; /* -21..21 when touching */ + if (off < -(PG_PH + PG_BALL) / 2 || off > (PG_PH + PG_BALL) / 2) + return false; + int32_t speed = (p->vx < 0 ? -p->vx : p->vx) + PG_S / 6; + if (speed > PG_VMAX) + speed = PG_VMAX; + p->bx = face; + p->vx = side ? -speed : speed; + /* Steeper return off the paddle's ends */ + p->vy = off * PG_S / 4; + /* The other paddle picks a fresh aiming error for this approach */ + p->aim[!side] = game_rnd_below(g, PG_PH - 6) - (PG_PH - 6) / 2; + g->events++; + game_set_score(g, g->score + 1); + return true; +} + +static void pong_tick(game_t *g) +{ + pong_t *p = &g->u.pong; + ui_rect_t f = pong_field(); + ui_rect_t old = pong_ball_rect(p); + + if (p->serve > 0) { + if (--p->serve == 0) { + p->vx = p->serve_right ? PG_V0 : -PG_V0; + p->vy = (game_rnd_below(g, 7) - 3) * PG_S / 2; + p->aim[0] = p->aim[1] = 0; + p->serve_right = !p->serve_right; + } + } else { + p->bx += p->vx; + p->by += p->vy; + int32_t top = f.top * PG_S, bottom = (f.bottom - PG_BALL) * PG_S; + if (p->by < top) { + p->by = 2 * top - p->by; + p->vy = -p->vy; + } else if (p->by > bottom) { + p->by = 2 * bottom - p->by; + p->vy = -p->vy; + } + if (!pong_hit(g, 0)) + pong_hit(g, 1); + if (p->bx < f.left * PG_S || p->bx + PG_BALL * PG_S > f.right * PG_S) { + /* Missed: the rally is over, serve again from the center */ + game_set_score(g, 0); + pong_center_ball(p); + } + } + ui_rect_t now = pong_ball_rect(p); + if (memcmp(&old, &now, sizeof(old))) + game_mark(g, ui_rect_union(old, now)); + pong_paddle(g, 0); + pong_paddle(g, 1); +} + +static void pong_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) +{ + const pong_t *p = &g->u.pong; + ui_rect_t f = pong_field(); + twin_coord_t cx = (f.left + f.right) / 2; + + /* Dashed center line, only the dashes inside the clip */ + if (clip.left < cx + 1 && cx - 1 < clip.right) { + int first = (clip.top - f.top) / 16; + for (int i = first < 0 ? 0 : first;; i++) { + ui_rect_t d = ui_rect(cx - 1, f.top + 4 + i * 16, 2, 8); + if (d.top >= clip.bottom || d.bottom > f.bottom) + break; + ui_fill(px, d, C_DIM); + } + } + for (int side = 0; side < 2; side++) { + ui_rect_t r = pong_paddle_rect(p, side); + if (ui_intersects(r, clip)) + ui_rrect(px, r, 3, UI_RGB(0xf0f4f2)); + } + ui_rect_t b = pong_ball_rect(p); + if (ui_intersects(b, clip)) + ui_circle(px, b.left + PG_BALL / 2, b.top + PG_BALL / 2, PG_BALL / 2, + UI_RGB(0x4dd0e1)); +} + +/* 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, +}; diff --git a/apps/cellphone/settings.c b/apps/cellphone/settings.c new file mode 100644 index 00000000..f2bcadf0 --- /dev/null +++ b/apps/cellphone/settings.c @@ -0,0 +1,951 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Settings: a root list and one sub-screen per category. + * + * Every screen is the same kind of view driven by a small item table built + * when the screen opens (sections, links, radios, switches, sliders, info + * rows, notes and the wallpaper grid). Layout is recomputed from the view + * size on every paint and event, so the screen follows the window manager + * when it is resized (the 'none' scene gives views the whole display). + * + * Only user preferences outlive a screen; they sit in one static struct so + * that sliders and switches keep their values across visits. + */ + +#include +#include + +#include "phone.h" + +#define MAX_ITEMS 20 +#define VAL_LEN 16 +#define HEADER_H PHONE_HEADER_H +#define ROW_H PHONE_LIST_ROW_H +#define RADIO_SUB_H 46 +#define SLIDER_H 52 +#define INFO_H 30 +#define CLOCK_H 86 +#define PLATE 26 +#define ICON_COL 40 /* icon column on the grouped surface */ +#define TILE_GAP 12 +#define TILE_LABEL_H 20 +#define TILE_R 8 + +/* Preferences shared by every visit to Settings (visual only) */ +static struct { + int brightness; /* 0..1000 */ + int volume; /* 0..1000 */ + bool vibrate; + bool keypad_tones; + bool clock_24h; +} prefs = {700, 500, true, false, true}; + +enum { PREF_BRIGHTNESS, PREF_VOLUME, PREF_VIBRATE, PREF_KEYPAD, PREF_24H }; + +typedef enum { + PAGE_ROOT, + PAGE_DISPLAY, + PAGE_SOUND, + PAGE_WALLPAPER, + PAGE_TIME, + PAGE_ABOUT, +} page_id_t; + +static const char *const page_titles[] = { + "Settings", "Display", "Sound", "Wallpaper", "Date & time", "About", +}; + +typedef enum { + IT_SECTION, + IT_LINK, /* id: page */ + IT_RADIO, /* arg: theme */ + IT_SWITCH, /* id: pref */ + IT_SLIDER, /* id: pref */ + IT_INFO, /* id: value */ + IT_NOTE, /* sub: wrapped text */ + IT_WALLPAPERS, /* grid of wallpaper tiles */ + IT_CLOCK, /* large time and date */ +} item_kind_t; + +/* Info values; those from VAL_HEAP on are refreshed once per second */ +enum { + VAL_NAME, + VAL_SCREEN, + VAL_TZ, + VAL_HEAP, + VAL_PEAK, + VAL_HITS, + VAL_MISSES, + VAL_RATE, + VAL_BYTES, + VAL_FRAMES, + VAL_PIXELS, +}; + +typedef struct { + item_kind_t kind; + int id, arg; + const char *sym; + const char *label; + const char *sub; +} item_t; + +typedef struct { + ui_view_t view; + page_id_t page; + item_t items[MAX_ITEMS]; + int n; + /* Layout in content coordinates, refreshed from the view size */ + twin_coord_t y[MAX_ITEMS], h[MAX_ITEMS]; + ui_scroll_t scroll; + int pressed; /* item under the finger, -1 when none */ + int pressed_tile; /* wallpaper tile under the finger */ + int grab; /* slider item being dragged, -1 when none */ + int minute; /* last minute shown by the clock */ + int refreshes; /* About: value refresh ticks (tests) */ + char vals[MAX_ITEMS][VAL_LEN]; /* About: live values last painted */ +} page_t; + +static void settings_paint(ui_view_t *v, twin_pixmap_t *px); +static void open_page(page_id_t page); + +static int *pref_int(int id) +{ + return id == PREF_BRIGHTNESS ? &prefs.brightness : &prefs.volume; +} + +static bool *pref_bool(int id) +{ + switch (id) { + case PREF_VIBRATE: + return &prefs.vibrate; + case PREF_KEYPAD: + return &prefs.keypad_tones; + default: + return &prefs.clock_24h; + } +} + +/* + * Formatting (integers only) + */ +static void fmt_bytes(char *buf, size_t len, uint64_t b) +{ + if (b < 1024) + snprintf(buf, len, "%u B", (unsigned) b); + else if (b < 1024 * 1024) + snprintf(buf, len, "%u.%u KB", (unsigned) (b / 1024), + (unsigned) (b * 10 / 1024 % 10)); + else + snprintf(buf, len, "%u.%u MB", (unsigned) (b >> 20), + (unsigned) ((b * 10 >> 20) % 10)); +} + +static void fmt_count(char *buf, size_t len, uint64_t n) +{ + if (n < 100000) + snprintf(buf, len, "%u", (unsigned) n); + else if (n < 10000000) + snprintf(buf, len, "%u.%uK", (unsigned) (n / 1000), + (unsigned) (n / 100 % 10)); + else + snprintf(buf, len, "%u.%uM", (unsigned) (n / 1000000), + (unsigned) (n / 100000 % 10)); +} + +static void format_value(int id, char *buf, size_t len) +{ + twin_memory_info_t mem; + twin_cache_stats_t cache; + twin_screen_t *screen = phone_screen(); + + switch (id) { + case VAL_NAME: + snprintf(buf, len, "Mado Phone"); + return; + case VAL_SCREEN: + snprintf(buf, len, "%dx%d", PHONE_W, PHONE_H); + return; + case VAL_TZ: + snprintf(buf, len, "GMT+8 Taipei"); + return; + case VAL_HEAP: + case VAL_PEAK: + twin_memory_get_info(&mem); + if (!mem.peak_bytes) /* CONFIG_MEMORY_STATS is off */ + snprintf(buf, len, "n/a"); + else + fmt_bytes(buf, len, + id == VAL_HEAP ? mem.current_bytes : mem.peak_bytes); + return; + case VAL_HITS: + case VAL_MISSES: + case VAL_RATE: + case VAL_BYTES: + twin_cache_get_stats(&cache); + if (id == VAL_HITS) { + fmt_count(buf, len, cache.hits); + } else if (id == VAL_MISSES) { + fmt_count(buf, len, cache.misses); + } else if (id == VAL_BYTES) { + fmt_bytes(buf, len, cache.bytes); + } else { + uint64_t total = (uint64_t) cache.hits + cache.misses; + unsigned pm = total ? (unsigned) (cache.hits * 1000ULL / total) : 0; + snprintf(buf, len, "%u.%u%%", pm / 10, pm % 10); + } + return; + case VAL_FRAMES: + fmt_count(buf, len, screen ? twin_screen_stats(screen)->frames : 0); + return; + case VAL_PIXELS: + fmt_count(buf, len, screen ? twin_screen_stats(screen)->pixels : 0); + return; + default: + buf[0] = 0; + } +} + +static bool is_live(const item_t *it) +{ + return it->kind == IT_INFO && it->id >= VAL_HEAP; +} + +/* + * Item table + */ +static void add(page_t *s, + item_kind_t kind, + int id, + int arg, + const char *sym, + const char *label, + const char *sub) +{ + if (s->n >= MAX_ITEMS) + return; + s->items[s->n++] = (item_t) {kind, id, arg, sym, label, sub}; +} + +static void section(page_t *s, const char *label) +{ + add(s, IT_SECTION, 0, 0, NULL, label, NULL); +} + +static void info(page_t *s, const char *label, int id) +{ + add(s, IT_INFO, id, 0, NULL, label, NULL); +} + +static void build(page_t *s) +{ + switch (s->page) { + case PAGE_ROOT: + section(s, "Device"); + add(s, IT_LINK, PAGE_DISPLAY, 0, TWIN_SYM_SUN, "Display", NULL); + add(s, IT_LINK, PAGE_SOUND, 0, TWIN_SYM_VOLUME, "Sound", NULL); + add(s, IT_LINK, PAGE_WALLPAPER, 0, TWIN_SYM_IMAGE, "Wallpaper", NULL); + section(s, "System"); + add(s, IT_LINK, PAGE_TIME, 0, TWIN_SYM_CLOCK, "Date & time", NULL); + add(s, IT_LINK, PAGE_ABOUT, 0, TWIN_SYM_LIST, "About", NULL); + break; + case PAGE_DISPLAY: + section(s, "Theme"); + for (int i = 0; i < phone_theme_count(); i++) + add(s, IT_RADIO, 0, i, NULL, phone_theme_at(i)->name, NULL); + section(s, "Screen"); + add(s, IT_SLIDER, PREF_BRIGHTNESS, 0, NULL, "Brightness", NULL); + break; + case PAGE_SOUND: + section(s, "Audio"); + add(s, IT_SLIDER, PREF_VOLUME, 0, NULL, "Volume", NULL); + section(s, "Feedback"); + add(s, IT_SWITCH, PREF_VIBRATE, 0, TWIN_SYM_BELL, "Vibrate", NULL); + add(s, IT_SWITCH, PREF_KEYPAD, 0, TWIN_SYM_KEYBOARD, "Keypad tones", + NULL); + break; + case PAGE_WALLPAPER: + add(s, IT_WALLPAPERS, 0, 0, NULL, "Wallpapers", NULL); + break; + case PAGE_TIME: + add(s, IT_CLOCK, 0, 0, NULL, "Clock", NULL); + section(s, "Format"); + add(s, IT_SWITCH, PREF_24H, 0, TWIN_SYM_CLOCK, "24-hour time", NULL); + section(s, "Region"); + add(s, IT_INFO, VAL_TZ, 0, TWIN_SYM_GLOBE, "Time zone", NULL); + break; + case PAGE_ABOUT: + section(s, "Device"); + info(s, "Name", VAL_NAME); + info(s, "Screen", VAL_SCREEN); + section(s, "Memory"); + info(s, "Heap in use", VAL_HEAP); + info(s, "Heap peak", VAL_PEAK); + section(s, "Coverage cache"); + info(s, "Hits", VAL_HITS); + info(s, "Misses", VAL_MISSES); + info(s, "Hit rate", VAL_RATE); + info(s, "Resident", VAL_BYTES); + section(s, "Compositor"); + info(s, "Frames", VAL_FRAMES); + info(s, "Pixels", VAL_PIXELS); + break; + } +} + +/* + * Layout + */ +static int grid_cols(twin_coord_t w) +{ + int cols = w / 110; + return cols < 2 ? 2 : cols; +} + +static twin_coord_t tile_w(twin_coord_t w) +{ + int cols = grid_cols(w); + return (w - (cols + 1) * TILE_GAP) / cols; +} + +static twin_coord_t tile_h(twin_coord_t w) +{ + return tile_w(w) * 3 / 4; +} + +/* Wallpaper tile @i inside a grid item whose top-left is (0, top) */ +static ui_rect_t tile_rect(twin_coord_t w, twin_coord_t top, int i) +{ + int cols = grid_cols(w); + twin_coord_t tw = tile_w(w), th = tile_h(w); + return ui_rect(TILE_GAP + (i % cols) * (tw + TILE_GAP), + top + TILE_GAP + (i / cols) * (th + TILE_LABEL_H + 6), tw, + th); +} + +static twin_coord_t item_height(const item_t *it, twin_coord_t w) +{ + switch (it->kind) { + case IT_SECTION: + return PHONE_SECTION_H; + case IT_RADIO: + return it->sub ? RADIO_SUB_H : ROW_H; + case IT_SLIDER: + return SLIDER_H; + case IT_INFO: + return it->sym ? ROW_H : INFO_H; + case IT_NOTE: + return 16 + 15 * ui_text_wrap(NULL, FONT_SM, 0, 0, w - 2 * 12, 15, 0, + it->sub, NULL); + case IT_WALLPAPERS: { + int rows = (phone_wallpaper_count() + grid_cols(w) - 1) / grid_cols(w); + return TILE_GAP + rows * (tile_h(w) + TILE_LABEL_H + 6); + } + case IT_CLOCK: + return CLOCK_H; + case IT_LINK: + case IT_SWITCH: + default: + return ROW_H; + } +} + +static void layout(page_t *s) +{ + twin_coord_t w = ui_view_width(&s->view), h = ui_view_height(&s->view); + twin_coord_t y = 0; + for (int i = 0; i < s->n; i++) { + s->y[i] = y; + s->h[i] = item_height(&s->items[i], w); + y += s->h[i]; + } + ui_scroll_setup(&s->scroll, ui_rect(0, HEADER_H, w, h - HEADER_H), y); +} + +/* Item rectangle in view coordinates (scrolled) */ +static ui_rect_t item_rect(page_t *s, int i) +{ + return ui_rect(0, s->scroll.area.top + s->y[i] - s->scroll.offset, + ui_w(s->scroll.area), s->h[i]); +} + +/* Part of @r inside the scroll viewport: what invalidation should cover */ +static ui_rect_t visible(page_t *s, ui_rect_t r) +{ + ui_rect_t a = s->scroll.area; + if (r.top < a.top) + r.top = a.top; + if (r.bottom > a.bottom) + r.bottom = a.bottom; + if (r.bottom < r.top) + r.bottom = r.top; + return r; +} + +static void invalidate_item(page_t *s, int i) +{ + ui_rect_t r = visible(s, item_rect(s, i)); + if (ui_h(r) > 0) + ui_view_invalidate(&s->view, r); +} + +static ui_rect_t slider_rect(ui_rect_t row) +{ + return ui_rect(row.left + 4, row.top + 24, ui_w(row) - 8, 24); +} + +static ui_rect_t value_rect(ui_rect_t row) +{ + return ui_rect(row.right - 120, row.top, 120, ui_h(row) - 1); +} + +/* + * Painting + */ + +/* Symbolic icon in the row's icon column, on the grouped surface */ +static void paint_icon_column(twin_pixmap_t *px, ui_rect_t r, const char *sym) +{ + const phone_theme_t *th = phone_theme(); + twin_coord_t cy = (r.top + r.bottom) / 2; + ui_fill(px, ui_rect(r.left, r.top, ICON_COL, ui_h(r)), th->group); + if (sym) + ui_icon(px, sym, FONT_NORMAL, r.left + ICON_COL / 2, cy, th->text); +} + +/* Theme preview: the theme's background with its primary color on it */ +static void paint_swatch(twin_pixmap_t *px, twin_coord_t cy, int theme) +{ + const phone_theme_t *t = phone_theme_at(theme); + ui_rect_t plate = + ui_rect((ICON_COL - PLATE) / 2, cy - PLATE / 2, PLATE, PLATE); + ui_rrect(px, plate, 7, phone_theme()->separator); + ui_rrect_grad(px, ui_inset(plate, 1), 6, t->bg, t->bg_grad); + ui_rrect(px, ui_rect(plate.left + 5, cy - 5, PLATE - 10, 10), 3, t->card); + ui_circle(px, plate.left + 9, cy, 3, t->primary); +} + +static bool radio_on(const item_t *it) +{ + return phone_theme_index() == it->arg; +} + +static void link_detail(const item_t *it, char *buf, size_t len) +{ + buf[0] = 0; + switch (it->id) { + case PAGE_DISPLAY: + snprintf(buf, len, "%s", phone_theme()->name); + break; + case PAGE_SOUND: + snprintf(buf, len, "%d%%", prefs.volume / 10); + break; + case PAGE_WALLPAPER: + snprintf(buf, len, "%s", phone_wallpaper_name(phone_wallpaper())); + break; + default: + break; + } +} + +static void paint_row(page_t *s, twin_pixmap_t *px, int i, ui_rect_t r) +{ + const phone_theme_t *th = phone_theme(); + const item_t *it = &s->items[i]; + twin_coord_t cy = (r.top + r.bottom) / 2; + twin_coord_t x = 14, right = r.right - 10; + char buf[24]; + + /* Rows with an icon column fill the rest of the row only */ + bool column = it->sym || it->kind == IT_RADIO; + ui_rect_t body = r; + if (column) + body.left += ICON_COL; + ui_fill(px, body, + i == s->pressed ? ui_mix(th->card, th->primary, 70) : th->card); + if (column) { + paint_icon_column(px, r, it->sym); + if (it->kind == IT_RADIO) + paint_swatch(px, cy, it->arg); + x = ICON_COL + 10; + } + + switch (it->kind) { + case IT_LINK: + ui_icon(px, TWIN_SYM_RIGHT, FONT_SM, r.right - 14, cy, th->text_sec); + link_detail(it, buf, sizeof(buf)); + right = r.right - 26; + /* The label wins: drop the detail when both do not fit */ + if (buf[0] && x + ui_text_width(FONT_NORMAL, it->label) + 12 + + ui_text_width(FONT_SM, buf) <= + right) { + ui_text_in(px, FONT_SM, + ui_rect(r.left, r.top, right - r.left, ui_h(r)), + UI_RIGHT, th->text_sec, buf); + right -= ui_text_width(FONT_SM, buf) + 8; + } + break; + case IT_SWITCH: + ui_switch(px, ui_rect(r.left, r.top, ui_w(r) - 12, ui_h(r)), + *pref_bool(it->id)); + right = r.right - 54; + break; + case IT_RADIO: + if (radio_on(it)) { + ui_circle(px, r.right - 22, cy, 9, th->primary); + ui_circle(px, r.right - 22, cy, 4, th->on_primary); + } else { + ui_ring(px, r.right - 22, cy, 8, 2, th->indicator); + } + right = r.right - 36; + break; + case IT_INFO: { + const char *val = s->vals[i]; + if (!is_live(it)) { + format_value(it->id, buf, sizeof(buf)); + val = buf; + } + ui_text_in(px, FONT_NORMAL, + ui_rect(r.left, r.top, ui_w(r) - 8, ui_h(r)), UI_RIGHT, + th->text_sec, val); + right = r.right - 8 - ui_text_width(FONT_NORMAL, val) - 8; + break; + } + default: + break; + } + + ui_rect_t text = {(twin_coord_t) (x - 4), right, r.top, r.bottom}; + if (it->sub) { + ui_rect_t top = {text.left, text.right, (twin_coord_t) (r.top + 3), + (twin_coord_t) (cy + 1)}; + ui_rect_t bot = {text.left, text.right, (twin_coord_t) (cy - 1), + (twin_coord_t) (r.bottom - 3)}; + ui_text_fit(px, FONT_NORMAL, top, UI_LEFT, th->text, it->label); + ui_text_fit(px, FONT_SM, bot, UI_LEFT, th->text_sec, it->sub); + } else { + ui_text_fit(px, FONT_NORMAL, text, UI_LEFT, th->text, it->label); + } + ui_separator(px, ui_rect(x, r.top, r.right - x, ui_h(r))); +} + +static void paint_slider(page_t *s, twin_pixmap_t *px, int i, ui_rect_t r) +{ + const phone_theme_t *th = phone_theme(); + const item_t *it = &s->items[i]; + int value = *pref_int(it->id); + char pct[16]; + snprintf(pct, sizeof(pct), "%d%%", value / 10); + ui_fill(px, r, th->card); + ui_text(px, FONT_NORMAL, 12, r.top + 18, UI_LEFT, th->text, it->label); + ui_text(px, FONT_SM, r.right - 12, r.top + 18, UI_RIGHT, th->text_sec, pct); + ui_slider(px, slider_rect(r), value, s->grab == i); + ui_separator(px, r); +} + +static void paint_note(page_t *s, twin_pixmap_t *px, int i, ui_rect_t r) +{ + const phone_theme_t *th = phone_theme(); + ui_fill(px, r, th->bg); + ui_text_wrap(px, FONT_SM, 12, r.top + 18, ui_w(r) - 24, 15, th->text_sec, + s->items[i].sub, NULL); +} + +static void paint_clock(twin_pixmap_t *px, ui_rect_t r) +{ + const phone_theme_t *th = phone_theme(); + char t[16], d[32]; + if (prefs.clock_24h) { + phone_format_time(t, sizeof(t)); + } else { + phone_clock_t c; + phone_clock(&c); + int h12 = c.hour % 12 ? c.hour % 12 : 12; + snprintf(t, sizeof(t), "%d:%02d %s", h12, c.min, + c.hour < 12 ? "AM" : "PM"); + } + phone_format_date(d, sizeof(d)); + twin_coord_t cx = (r.left + r.right) / 2; + ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 128)); + ui_text(px, FONT_CLOCK, cx, r.top + 46, UI_CENTER, th->text, t); + ui_text(px, FONT_SM, cx, r.top + 70, UI_CENTER, th->text_sec, d); + ui_separator(px, r); +} + +static void paint_wallpapers(page_t *s, + twin_pixmap_t *px, + ui_rect_t r, + ui_rect_t clip) +{ + const phone_theme_t *th = phone_theme(); + twin_coord_t w = ui_w(r); + ui_fill(px, r, th->bg); + for (int i = 0; i < phone_wallpaper_count(); i++) { + ui_rect_t t = tile_rect(w, r.top, i); + ui_rect_t cell = ui_rect(t.left - 6, t.top - 6, ui_w(t) + 12, + ui_h(t) + TILE_LABEL_H + 8); + if (!ui_intersects(cell, clip)) + continue; + bool sel = i == phone_wallpaper(); + twin_argb32_t label = phone_paint_wallpaper(px, t, i); + /* Hint of the home grid so the tile reads as a screen */ + twin_coord_t u = ui_w(t) / 9; + for (int k = 0; k < 6; k++) + ui_rrect(px, + ui_rect(t.left + u + (k % 3) * 3 * u, + t.top + u + (k / 3) * 3 * u, u * 2, u * 2), + u / 2, ui_alpha(label, 0x50)); + if (i == s->pressed_tile) + ui_fill(px, t, ui_alpha(UI_RGB(0x000000), 0x30)); + /* Round the corners by painting the background around them */ + ui_rrect_stroke(px, ui_inset(t, -6), TILE_R + 6, 6, th->bg); + if (sel) { + ui_rrect_stroke(px, ui_inset(t, -3), TILE_R + 3, 2, th->primary); + ui_circle(px, t.right - 10, t.top + 10, 7, th->primary); + ui_icon(px, TWIN_SYM_CHECK, 9, t.right - 10, t.top + 10, + th->on_primary); + } else { + ui_rrect_stroke(px, t, TILE_R, 1, ui_alpha(th->text, 0x30)); + } + ui_text_in( + px, FONT_SM, ui_rect(t.left, t.bottom + 2, ui_w(t), TILE_LABEL_H), + UI_CENTER, sel ? th->primary : th->text, phone_wallpaper_name(i)); + } +} + +static void settings_paint(ui_view_t *v, twin_pixmap_t *px) +{ + page_t *s = (page_t *) v; + const phone_theme_t *th = phone_theme(); + ui_rect_t clip = ui_view_clip(v); + layout(s); + ui_rect_t area = s->scroll.area; + + if (ui_intersects(clip, area)) { + twin_coord_t bottom = area.top; + for (int i = 0; i < s->n; i++) { + ui_rect_t r = item_rect(s, i); + bottom = r.bottom; + if (r.bottom <= area.top || r.top >= area.bottom || + !ui_intersects(r, clip)) + continue; + switch (s->items[i].kind) { + case IT_SECTION: + ui_section(px, r, s->items[i].label); + break; + case IT_SLIDER: + paint_slider(s, px, i, r); + break; + case IT_NOTE: + paint_note(s, px, i, r); + break; + case IT_CLOCK: + paint_clock(px, r); + break; + case IT_WALLPAPERS: + paint_wallpapers(s, px, r, clip); + break; + default: + paint_row(s, px, i, r); + break; + } + } + if (bottom < area.bottom) + ui_fill(px, ui_rect(0, bottom, ui_w(area), area.bottom - bottom), + th->bg); + ui_scroll_bar(px, &s->scroll); + } + /* Last, so rows scrolled under the header never show through */ + if (clip.top < HEADER_H) + ui_header(px, ui_rect(0, 0, ui_view_width(&s->view), HEADER_H), NULL, + s->view.title, NULL); +} + +/* + * Behavior + */ + +/* Repaint the other Settings screens: their details mirror this change */ +static void refresh_others(page_t *self) +{ + if (!phone_screen()) + return; + for (twin_window_t *w = twin_screen_first_window(phone_screen()); w; + w = twin_window_next(w)) { + ui_view_t *o = ui_view_of(w); + if (o && o != &self->view && o->paint == settings_paint) + ui_view_invalidate_all(o); + } +} + +static int item_at(page_t *s, twin_coord_t x, twin_coord_t y) +{ + if (!ui_hit(s->scroll.area, x, y)) + return -1; + for (int i = 0; i < s->n; i++) + if (ui_hit(item_rect(s, i), x, y)) + return i; + return -1; +} + +static int tile_at(page_t *s, int i, twin_coord_t x, twin_coord_t y) +{ + ui_rect_t r = item_rect(s, i); + for (int k = 0; k < phone_wallpaper_count(); k++) { + ui_rect_t t = tile_rect(ui_w(r), r.top, k); + t.bottom += TILE_LABEL_H; + if (ui_hit(t, x, y)) + return k; + } + return -1; +} + +static bool tappable(const item_t *it) +{ + return it->kind == IT_LINK || it->kind == IT_RADIO || + it->kind == IT_SWITCH || it->kind == IT_WALLPAPERS; +} + +static void set_pressed(page_t *s, int i, int tile) +{ + if (i == s->pressed && tile == s->pressed_tile) + return; + if (s->pressed >= 0) + invalidate_item(s, s->pressed); + s->pressed = i; + s->pressed_tile = tile; + if (i >= 0) + invalidate_item(s, i); +} + +static void slide(page_t *s, twin_coord_t x) +{ + int *val = pref_int(s->items[s->grab].id); + int nv = ui_slider_value_at(slider_rect(item_rect(s, s->grab)), x); + if (nv != *val) { + *val = nv; + invalidate_item(s, s->grab); + } +} + +static void activate(page_t *s, int i, int tile) +{ + item_t *it = &s->items[i]; + switch (it->kind) { + case IT_LINK: + open_page((page_id_t) it->id); + break; + case IT_RADIO: + if (radio_on(it)) + break; + phone_theme_set(it->arg); /* repaints every window */ + break; + case IT_SWITCH: { + bool *b = pref_bool(it->id); + *b = !*b; + invalidate_item(s, i); + /* The 24-hour switch also changes the clock above it */ + if (it->id == PREF_24H) + for (int k = 0; k < s->n; k++) + if (s->items[k].kind == IT_CLOCK) + invalidate_item(s, k); + break; + } + case IT_WALLPAPERS: + if (tile >= 0 && tile != phone_wallpaper()) { + phone_set_wallpaper(tile); + invalidate_item(s, i); + refresh_others(s); + } + break; + default: + break; + } +} + +static bool settings_event(ui_view_t *v, const ui_event_t *e) +{ + page_t *s = (page_t *) v; + layout(s); + + /* A grabbed slider owns the gesture, whatever its direction */ + if (s->grab >= 0) { + int g = s->grab; + if (e->kind == UI_MOVE || e->kind == UI_DRAG) { + slide(s, e->x); + return true; + } + if (e->kind == UI_RELEASE || e->kind == UI_TAP || + e->kind == UI_CANCEL) { + s->grab = -1; + invalidate_item(s, g); + refresh_others(s); + return true; + } + } + + if (ui_scroll_event(v, &s->scroll, e)) { + set_pressed(s, -1, -1); + return true; + } + + switch (e->kind) { + case UI_PRESS: { + int i = item_at(s, e->x, e->y); + if (i >= 0 && s->items[i].kind == IT_SLIDER && + ui_hit(slider_rect(item_rect(s, i)), e->x, e->y)) { + s->grab = i; + slide(s, e->x); + invalidate_item(s, i); + return true; + } + if (i >= 0 && tappable(&s->items[i])) { + int tile = s->items[i].kind == IT_WALLPAPERS + ? tile_at(s, i, e->x, e->y) + : -1; + if (s->items[i].kind != IT_WALLPAPERS || tile >= 0) + set_pressed(s, i, tile); + } + return true; + } + case UI_TAP: { + int i = s->pressed, tile = s->pressed_tile; + set_pressed(s, -1, -1); + if (i >= 0 && i == item_at(s, e->x, e->y)) + activate(s, i, tile); + return true; + } + case UI_MOVE: + return true; + default: + set_pressed(s, -1, -1); + return false; + } +} + +/* About: refresh the live values, repainting only those that changed */ +static twin_time_t about_tick(ui_view_t *v) +{ + page_t *s = (page_t *) v; + char buf[VAL_LEN]; + layout(s); + for (int i = 0; i < s->n; i++) { + if (!is_live(&s->items[i])) + continue; + format_value(s->items[i].id, buf, sizeof(buf)); + if (!strcmp(buf, s->vals[i])) + continue; + strcpy(s->vals[i], buf); + ui_rect_t r = visible(s, value_rect(item_rect(s, i))); + if (ui_h(r) > 0) + ui_view_invalidate(v, r); + } + s->refreshes++; + return 1000; +} + +/* Date & time: repaint the clock when the minute turns */ +static twin_time_t clock_tick(ui_view_t *v) +{ + page_t *s = (page_t *) v; + phone_clock_t c; + phone_clock(&c); + int minute = c.hour * 60 + c.min; + if (minute != s->minute) { + s->minute = minute; + layout(s); + invalidate_item(s, 0); + } + return 1000; +} + +static void open_page(page_id_t page) +{ + page_t *s = ui_view_new(sizeof(*s), page_titles[page], settings_paint, + settings_event, NULL); + if (!s) + return; + s->page = page; + s->pressed = s->pressed_tile = s->grab = -1; + build(s); + if (page == PAGE_ABOUT) { + for (int i = 0; i < s->n; i++) + if (is_live(&s->items[i])) + format_value(s->items[i].id, s->vals[i], VAL_LEN); + ui_view_every(&s->view, 1000, about_tick); + } else if (page == PAGE_TIME) { + phone_clock_t c; + phone_clock(&c); + s->minute = c.hour * 60 + c.min; + ui_view_every(&s->view, 1000, clock_tick); + } + ui_view_show(&s->view); +} + +void app_settings_open(void) +{ + open_page(PAGE_ROOT); +} + +/* + * Test hooks + */ + +/* Rectangle (view coordinates) of the item or wallpaper tile labelled + * @label on the Settings screen on top; false if there is none */ +bool app_settings_item_rect(const char *label, ui_rect_t *out) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != settings_paint) + return false; + page_t *s = (page_t *) v; + layout(s); + for (int i = 0; i < s->n; i++) { + const item_t *it = &s->items[i]; + if (it->kind == IT_WALLPAPERS) { + ui_rect_t r = item_rect(s, i); + for (int k = 0; k < phone_wallpaper_count(); k++) { + if (strcmp(phone_wallpaper_name(k), label)) + continue; + *out = tile_rect(ui_w(r), r.top, k); + return true; + } + } else if (it->kind != IT_SECTION && it->label && + !strcmp(it->label, label)) { + *out = it->kind == IT_SLIDER ? slider_rect(item_rect(s, i)) + : item_rect(s, i); + return true; + } + } + return false; +} + +/* Current preference value by name; -1 if unknown */ +int app_settings_pref(const char *name) +{ + if (!strcmp(name, "brightness")) + return prefs.brightness; + if (!strcmp(name, "volume")) + return prefs.volume; + if (!strcmp(name, "vibrate")) + return prefs.vibrate; + if (!strcmp(name, "keypad")) + return prefs.keypad_tones; + if (!strcmp(name, "24h")) + return prefs.clock_24h; + return -1; +} + +/* Number of About value refreshes on the screen on top; -1 if not About */ +int app_settings_about_refreshes(void) +{ + ui_view_t *v = phone_top(); + if (!v || v->paint != settings_paint || ((page_t *) v)->page != PAGE_ABOUT) + return -1; + return ((page_t *) v)->refreshes; +} diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c new file mode 100644 index 00000000..dea789a4 --- /dev/null +++ b/apps/cellphone/shell.c @@ -0,0 +1,786 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone shell: themes, clock, app registry, system bars, navigation. + * + * The status bar and navigation bar are plain windows registered with the + * mobile window manager as system bars; every screen above them is a view. + * Navigation is nothing more than showing and closing views: the window + * manager owns the stack, the transitions and the memory of hidden screens. + */ + +#include +#include +#include +#include + +#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 + */ +static twin_screen_t *g_screen; +static twin_window_t *g_status, *g_nav; +static ui_view_t *g_home; +static twin_timeout_t *g_tick; +static int g_wallpaper; +static int g_status_minute = -1; +static int g_nav_pressed = -1; +/* nav_state() the nav bar last painted in full, NAV_STALE if unknown */ +#define NAV_STALE (-100) +static int g_nav_drawn = NAV_STALE; + +twin_screen_t *phone_screen(void) +{ + return g_screen; +} + +static void _repaint_window(twin_window_t *w) +{ + if (!w) + return; + twin_rect_t c = twin_window_client(w); + twin_window_damage(w, c.left, c.top, c.right, c.bottom); +} + +static int nav_state(void); + +/* The view stack changed: repaint the nav bar if its tabs look different */ +void phone_view_changed(void) +{ + if (nav_state() != g_nav_drawn) + _repaint_window(g_nav); +} + +void phone_theme_set(int idx) +{ + if (idx < 0 || idx >= N_THEMES || !g_screen) + return; + theme_idx = idx; + for (twin_window_t *w = twin_screen_first_window(g_screen); w; + w = twin_window_next(w)) + _repaint_window(w); +} + +/* + * Wallpapers + */ +typedef struct { + const char *name; + twin_argb32_t top, bottom, label; +} wallpaper_t; + +static const wallpaper_t wallpapers[] = { + {"Theme", 0, 0, 0}, + {"Dusk", UI_RGB(0x4a3a78), UI_RGB(0xe08a5a), UI_RGB(0xffffff)}, + {"Ocean", UI_RGB(0x0f4c81), UI_RGB(0x46a6c8), UI_RGB(0xffffff)}, + {"Forest", UI_RGB(0x1f4d36), UI_RGB(0x7aa86a), UI_RGB(0xffffff)}, +}; + +#define N_WALLPAPERS ((int) (sizeof(wallpapers) / sizeof(wallpapers[0]))) + +int phone_wallpaper(void) +{ + return g_wallpaper; +} + +int phone_wallpaper_count(void) +{ + return N_WALLPAPERS; +} + +const char *phone_wallpaper_name(int idx) +{ + return (idx >= 0 && idx < N_WALLPAPERS) ? wallpapers[idx].name : ""; +} + +void phone_set_wallpaper(int idx) +{ + if (idx < 0 || idx >= N_WALLPAPERS) + return; + g_wallpaper = idx; + if (g_home) + ui_view_invalidate_all(g_home); +} + +twin_argb32_t phone_paint_wallpaper(twin_pixmap_t *px, ui_rect_t r, int idx) +{ + const phone_theme_t *th = phone_theme(); + if (idx <= 0 || idx >= N_WALLPAPERS) { + /* The grouped surface: the launcher's tiles stand out on it */ + ui_vgrad(px, r, th->group, ui_darken(th->group, 6)); + return th->text; + } + ui_vgrad(px, r, wallpapers[idx].top, wallpapers[idx].bottom); + return wallpapers[idx].label; +} + +/* + * Clock + */ +static void (*clock_source)(phone_clock_t *c); + +void phone_set_clock_source(void (*source)(phone_clock_t *c)) +{ + clock_source = source; +} + +void phone_clock(phone_clock_t *c) +{ + if (clock_source) { + clock_source(c); + return; + } + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + c->hour = tm.tm_hour; + c->min = tm.tm_min; + c->sec = tm.tm_sec; + c->wday = tm.tm_wday; + c->mday = tm.tm_mday; + c->mon = tm.tm_mon; +} + +void phone_format_time(char *buf, size_t len) +{ + phone_clock_t c; + phone_clock(&c); + snprintf(buf, len, "%02d:%02d", c.hour, c.min); +} + +void phone_format_date(char *buf, size_t len) +{ + static const char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"}; + static const char *months[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + phone_clock_t c; + phone_clock(&c); + snprintf(buf, len, "%s, %s %d", days[c.wday % 7], months[c.mon % 12], + c.mday); +} + +/* + * 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); + +static const phone_app_t apps[] = { + {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x5b8c45), app_dialer_open, NULL}, + {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x5a7896), app_contacts_open, + NULL}, + {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x4f8c7c), app_sms_open, NULL}, + {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xc08a3e), app_calc_open, + 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, + NULL}, + {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, + NULL}, + {"Snake", NULL, glyph_snake, UI_RGB(0x4d8f86), app_snake_open, NULL}, + {"Pong", NULL, glyph_pong, UI_RGB(0x646c9a), app_pong_open, NULL}, + {"Tetris", NULL, glyph_tetris, UI_RGB(0xad5348), app_tetris_open, NULL}, +}; + +#define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) + +/* Then every other app of the shared registry (apps.c), in registry order: + * a mobile build compiles in only apps that run under it. Listed at boot. */ +static phone_app_t demos[16]; +static int n_demos; + +static void _list_demos(void) +{ + static const twin_argb32_t accents[] = { + UI_RGB(0x3f7f78), UI_RGB(0x7c7468), UI_RGB(0x82689a), + UI_RGB(0x9a5f86), UI_RGB(0x7a8f3e), UI_RGB(0x4a7fa0), + }; + n_demos = 0; + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *e = apps_at(i); + if (e->boot) + continue; + if (n_demos == (int) (sizeof(demos) / sizeof(demos[0]))) + break; + demos[n_demos] = (phone_app_t) { + .name = e->name, + .sym = e->icon, + .accent = accents[n_demos % (sizeof(accents) / sizeof(accents[0]))], + .demo = e, + }; + n_demos++; + } +} + +int phone_app_count(void) +{ + return N_APPS + n_demos; +} + +const phone_app_t *phone_app_at(int idx) +{ + if (idx >= 0 && idx < N_APPS) + return &apps[idx]; + return idx >= N_APPS && idx < N_APPS + n_demos ? &demos[idx - N_APPS] + : NULL; +} + +int phone_app_find(const char *name) +{ + for (int i = 0; i < phone_app_count(); i++) + if (!strcmp(phone_app_at(i)->name, name)) + return i; + 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); + if (!a) + return; + /* Squircle plate: radius ~27% of the side, accent fading 14% darker */ + int radius = ui_w(plate) * 27 / 100; + twin_argb32_t top = a->accent, bottom = ui_darken(a->accent, 14); + if (pressed) { + top = ui_darken(top, 18); + bottom = ui_darken(bottom, 18); + } + ui_rrect(px, ui_rect(plate.left, plate.top + 2, ui_w(plate), ui_h(plate)), + radius, 0x28000000); + ui_rrect_grad(px, plate, radius, top, bottom); + /* A soft sheen over the upper half: the small tactile touch the + * launcher keeps, never repeated in toolbars or lists */ + ui_rrect_grad(px, + ui_rect(plate.left + 2, plate.top + 2, ui_w(plate) - 4, + ui_h(plate) * 2 / 5), + radius - 2, 0x2c2c2c2c, 0x04040404); + if (a->sym) + ui_icon(px, a->sym, ui_w(plate) / 2, (plate.left + plate.right) / 2, + (plate.top + plate.bottom) / 2, 0xffffffff); + else if (a->glyph) + a->glyph(px, plate, 0xffffffff); +} + +/* + * Status bar: signal, time, battery + */ +static void status_draw(twin_window_t *w) +{ + const phone_theme_t *th = phone_theme(); + twin_pixmap_t *px = twin_window_pixmap(w); + ui_rect_t r = ui_rect(0, 0, PHONE_W, PHONE_STATUS_H); + ui_vgrad(px, r, th->statusbar_grad, th->statusbar); + ui_icon(px, TWIN_SYM_SIGNAL, FONT_SM, 14, PHONE_STATUS_H / 2, th->on_bar); + ui_icon(px, TWIN_SYM_WIFI, FONT_SM, 34, PHONE_STATUS_H / 2, th->on_bar); + char t[8]; + phone_format_time(t, sizeof(t)); + ui_text_in(px, FONT_SM, r, UI_CENTER, th->on_bar, t); + ui_icon(px, TWIN_SYM_BATTERY, FONT_NORMAL, PHONE_W - 16, PHONE_STATUS_H / 2, + th->on_bar); +} + +static twin_time_t status_tick(twin_time_t now, void *closure) +{ + (void) now; + twin_window_t *w = closure; + phone_clock_t c; + phone_clock(&c); + int minute = c.hour * 60 + c.min; + if (minute != g_status_minute) { + g_status_minute = minute; + /* Only the time label changes */ + twin_window_damage(w, PHONE_W / 2 - 30, 0, PHONE_W / 2 + 30, + PHONE_STATUS_H); + } + return 1000; +} + +/* + * Navigation bar: Dialer | Home/Back | Contacts + */ +#define NAV_TABS 3 + +static int nav_active(void) +{ + ui_view_t *top = phone_top(); + if (top && top->app == phone_app_find("Phone")) + return 0; + if (top && top->app == phone_app_find("Contacts")) + return 2; + return phone_depth() > 0 ? -1 : 1; +} + +/* Everything the tabs show besides the press highlight and the theme */ +static int nav_state(void) +{ + return nav_active() * 2 + (phone_depth() > 0); +} + +static ui_rect_t nav_tab_rect(int i) +{ + twin_coord_t w = PHONE_W / NAV_TABS; + return ui_rect(i * w + 3, 3, w - 6, PHONE_NAV_H - 6); +} + +static void nav_draw(twin_window_t *w) +{ + const phone_theme_t *th = phone_theme(); + twin_pixmap_t *px = twin_window_pixmap(w); + ui_rect_t clip = twin_pixmap_get_clip(px); + bool full = clip.left <= 0 && clip.top <= 0 && clip.right >= PHONE_W && + clip.bottom >= PHONE_NAV_H; + g_nav_drawn = full ? nav_state() : NAV_STALE; + ui_vgrad(px, ui_rect(0, 0, PHONE_W, PHONE_NAV_H), th->navbar_grad, + th->navbar); + ui_fill(px, ui_rect(0, 0, PHONE_W, 1), th->separator); + bool back = phone_depth() > 0; + int active = nav_active(); + static const char *labels[NAV_TABS] = {"Dialer", "Home", "Contacts"}; + static const char *syms[NAV_TABS] = {TWIN_SYM_PHONE, TWIN_SYM_HOME, + TWIN_SYM_CONTACTS}; + for (int i = 0; i < NAV_TABS; i++) { + ui_rect_t r = nav_tab_rect(i); + /* The current page is filled with the accent; the rest are soft + * keys on the tray, so the selection never relies on color alone */ + bool on = i == active; + ui_button(px, r, 7, on ? UI_BTN_PRIMARY : UI_BTN_CARD, + i == g_nav_pressed, 0, NULL); + twin_argb32_t fg = on ? th->on_primary : th->text; + const char *sym = (i == 1 && back) ? TWIN_SYM_LEFT : syms[i]; + const char *label = (i == 1 && back) ? "Back" : labels[i]; + twin_coord_t cx = (r.left + r.right) / 2; + ui_icon(px, sym, FONT_NORMAL, cx, r.top + 12, fg); + ui_text(px, FONT_SM, cx, r.bottom - 5, UI_CENTER, fg, label); + } +} + +static int nav_hit(twin_coord_t x, twin_coord_t y) +{ + for (int i = 0; i < NAV_TABS; i++) + if (ui_hit(nav_tab_rect(i), x, y)) + return i; + 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; +} + +/* Destroy every app window except home and @keep, without animation */ +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)) + twin_window_destroy(w); + w = next; + } +} + +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; + /* Tabs are destinations, not history: reset to home, then open */ + _drop_apps(NULL); + phone_open(app); +} + +static bool nav_event(twin_window_t *w, twin_event_t *ev) +{ + switch (ev->kind) { + case TwinEventButtonDown: + g_nav_pressed = nav_hit(ev->u.pointer.x, ev->u.pointer.y); + _repaint_window(w); + return true; + case TwinEventButtonUp: { + int tab = nav_hit(ev->u.pointer.x, ev->u.pointer.y); + bool clicked = tab >= 0 && tab == g_nav_pressed; + g_nav_pressed = -1; + _repaint_window(w); + if (!clicked) + return true; + if (tab == 0) + nav_go_root("Phone"); + else if (tab == 2) + nav_go_root("Contacts"); + else if (phone_depth() > 0) + phone_back(); + return true; + } + case TwinEventMotion: + return true; + default: + return false; + } +} + +/* + * 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; +} + +int phone_depth(void) +{ + int n = 0; + if (!g_screen) + 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)) + n++; + return n; +} + +void phone_open(int app) +{ + const phone_app_t *a = phone_app_at(app); + if (!a) + return; + if (a->demo) { + /* Its own windows: the policy stacks them over the work area */ + a->demo->start(g_screen, a->demo->name, 0, PHONE_STATUS_H, PHONE_W, + PHONE_CONTENT_H); + phone_view_changed(); + return; + } + if (!a->open) + return; + a->open(); + /* Tag the view the app just pushed */ + ui_view_t *top = phone_top(); + if (top && top != g_home && top->app < 0) + top->app = app; + phone_view_changed(); +} + +void phone_back(void) +{ + twin_window_t *top = _top_app(); + if (top && !is_home(top)) + twin_window_back(top); +} + +void phone_home(void) +{ + twin_window_t *top = _top_app(); + if (!top || is_home(top)) + return; + /* Hidden screens go first (no animation), then the visible one pops */ + _drop_apps(top); + twin_window_close(top); +} + +static ui_view_t *lock_view(void) +{ + if (!g_screen) + return NULL; + for (twin_window_t *w = twin_screen_first_window(g_screen); w; + w = twin_window_next(w)) + if (ui_view_of(w) && twin_window_style(w) == TwinWindowFullscreen && + twin_window_is_mapped(w)) + return ui_view_of(w); + return NULL; +} + +bool phone_locked(void) +{ + return lock_view() != NULL; +} + +void phone_lock(void) +{ + if (phone_locked()) + return; + ui_view_t *lock = lock_create(); + if (lock) + ui_view_show(lock); +} + +/* + * Boot + */ +/* Whoever destroys the bars (shutdown, screen teardown) must not leave the + * shell pointing at them */ +static void bar_destroyed(twin_window_t *w) +{ + if (w == g_status) { + g_status = NULL; + if (g_tick) + twin_clear_timeout(g_tick); + g_tick = NULL; + } + if (w == g_nav) + g_nav = NULL; +} + +static void home_destroyed(ui_view_t *v) +{ + if (v == g_home) + g_home = NULL; +} + +static twin_window_t *bar_create(twin_coord_t y, + twin_coord_t h, + twin_draw_func_t draw, + twin_event_func_t event) +{ + twin_window_t *w = twin_window_create(g_screen, PHONE_FORMAT, + TwinWindowPlain, 0, y, PHONE_W, h); + if (!w) + return NULL; + twin_window_set_callbacks(w, draw, event, bar_destroyed); + twin_window_set_opaque(w, true); + twin_window_draw(w); + twin_window_show(w); + return w; +} + +bool phone_boot(twin_screen_t *screen) +{ + if (!ui_init()) + return false; + g_screen = screen; + _list_demos(); + + g_status = bar_create(0, PHONE_STATUS_H, status_draw, NULL); + g_nav = bar_create(PHONE_H - PHONE_NAV_H, PHONE_NAV_H, nav_draw, nav_event); + twin_screen_set_bars(screen, g_status, g_nav); + if (g_status) + g_tick = twin_set_timeout(status_tick, 1000, g_status); + + g_home = home_create(); + if (!g_status || !g_nav || !g_home) { + /* No shell without its bars and home: let the caller fall back */ + phone_shutdown(); + return false; + } + g_home->destroy = home_destroyed; + ui_view_show(g_home); + phone_lock(); + return true; +} + +void phone_shutdown(void) +{ + if (!g_screen) + return; + if (g_tick) + twin_clear_timeout(g_tick); + g_tick = NULL; + /* Views and the windows of apps opened from home first (top-down), + * then the bars */ + for (;;) { + twin_window_t *last = NULL; + for (twin_window_t *w = twin_screen_first_window(g_screen); w; + w = twin_window_next(w)) + if (ui_view_of(w) || twin_window_style(w) == TwinWindowApplication) + last = w; + if (!last) + break; + twin_window_destroy(last); + } + twin_screen_set_bars(g_screen, NULL, NULL); + if (g_status) + twin_window_destroy(g_status); + if (g_nav) + twin_window_destroy(g_nav); + g_status = g_nav = NULL; + g_home = NULL; + g_screen = NULL; + g_status_minute = -1; + g_nav_pressed = -1; + theme_idx = 0; + g_wallpaper = 0; + ui_fini(); +} diff --git a/apps/cellphone/sms.c b/apps/cellphone/sms.c new file mode 100644 index 00000000..a300b458 --- /dev/null +++ b/apps/cellphone/sms.c @@ -0,0 +1,666 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Messages: conversation list and chat. + * + * The chat lays its bubbles out once per layout change (open, keyboard + * shown or hidden, message sent) into per-message offsets in the view + * struct; painting walks that table and skips every bubble outside the + * clip, so a scroll step wraps and draws only the bubbles crossing the + * exposed band. Typing repaints the text field, and the send button only + * when its enabled state flips. + */ + +#include + +#include "data.h" +#include "phone.h" + +#define LIST_ROW_H 52 +#define LIST_AVATAR_R 17 + +#define COMPOSER_H 36 +#define FIELD_H 28 +#define SEND_W 40 + +#define BUBBLE_MAX_W (PHONE_W * 70 / 100) +#define BUBBLE_PAD_X 10 +#define BUBBLE_PAD_Y 6 +#define BUBBLE_R 12 +#define BUBBLE_TAIL 14 /* corner square with a tighter radius */ +#define LINE_H 17 +#define TIME_H 15 /* timestamp line under a bubble */ +#define MSG_GAP 5 +#define SIDE 8 +#define CHAT_PAD 8 + +/* Messages sent in this session are kept per open chat (stock is const) */ +#define MAX_EXTRA 8 +#define MAX_MSGS 32 + +/* + * Conversation list + */ +typedef struct { + ui_view_t view; + ui_scroll_t scroll; + int pressed; /* thread under the finger, -1 when none */ +} sms_list_t; + +/* + * Chat + */ +typedef struct { + twin_coord_t y; /* bubble top in content coordinates */ + twin_coord_t w, h; +} bubble_t; + +typedef struct { + char text[sizeof(((ui_field_t *) 0)->text)]; + char time[8]; +} sent_msg_t; + +typedef struct { + ui_view_t view; + const phone_sms_thread_t *thread; + int stock; /* stock messages shown (clamped to fit the table) */ + int n_extra; /* messages sent since the chat opened */ + sent_msg_t extra[MAX_EXTRA]; + bubble_t bubble[MAX_MSGS]; + twin_coord_t content_h; + ui_scroll_t scroll; + ui_field_t field; + ui_kbd_t kbd; + bool send_pressed; + bool send_enabled; /* last painted state, to repaint only on change */ + /* Smooth scroll to the newest message after a send */ + twin_coord_t glide_from, glide_to; + bool gliding; +} sms_chat_t; + +/* + * Chat geometry + */ +static int msg_count(const sms_chat_t *c) +{ + return c->stock + c->n_extra; +} + +static const char *msg_text(const sms_chat_t *c, int i) +{ + return i < c->stock ? c->thread->messages[i].text + : c->extra[i - c->stock].text; +} + +static const char *msg_time(const sms_chat_t *c, int i) +{ + return i < c->stock ? c->thread->messages[i].time + : c->extra[i - c->stock].time; +} + +static bool msg_sent(const sms_chat_t *c, int i) +{ + return i < c->stock ? c->thread->messages[i].is_sent != 0 : true; +} + +static twin_coord_t composer_top(const sms_chat_t *c) +{ + return ui_view_height(&c->view) - COMPOSER_H - + (c->kbd.visible ? UI_KBD_H : 0); +} + +static ui_rect_t composer_rect(const sms_chat_t *c) +{ + return ui_rect(0, composer_top(c), PHONE_W, COMPOSER_H); +} + +static ui_rect_t field_rect(const sms_chat_t *c) +{ + twin_coord_t top = composer_top(c) + (COMPOSER_H - FIELD_H) / 2; + return ui_rect(SIDE - 2, top, PHONE_W - 3 * (SIDE - 2) - SEND_W, FIELD_H); +} + +static ui_rect_t send_rect(const sms_chat_t *c) +{ + twin_coord_t top = composer_top(c) + (COMPOSER_H - FIELD_H) / 2; + return ui_rect(PHONE_W - (SIDE - 2) - SEND_W, top, SEND_W, FIELD_H); +} + +static ui_rect_t kbd_rect(const sms_chat_t *c) +{ + return ui_kbd_rect(ui_view_height(&c->view)); +} + +static ui_rect_t chat_area(const sms_chat_t *c) +{ + return ui_rect(0, PHONE_HEADER_H, PHONE_W, + composer_top(c) - PHONE_HEADER_H); +} + +/* Bubble rectangle of message @i in view coordinates at the current scroll */ +static ui_rect_t bubble_rect(const sms_chat_t *c, int i) +{ + const bubble_t *b = &c->bubble[i]; + twin_coord_t top = c->scroll.area.top + b->y - c->scroll.offset; + twin_coord_t left = msg_sent(c, i) ? PHONE_W - SIDE - b->w : SIDE; + return ui_rect(left, top, b->w, b->h); +} + +/* Measure every bubble; run on open and whenever a message is added */ +static void layout_messages(sms_chat_t *c) +{ + twin_coord_t y = CHAT_PAD; + for (int i = 0; i < msg_count(c); i++) { + twin_coord_t widest = 0; + int lines = ui_text_wrap(NULL, FONT_NORMAL, 0, 0, + BUBBLE_MAX_W - 2 * BUBBLE_PAD_X, LINE_H, 0, + msg_text(c, i), &widest); + if (lines < 1) + lines = 1; + bubble_t *b = &c->bubble[i]; + b->y = y; + b->w = widest + 2 * BUBBLE_PAD_X; + if (b->w < 2 * BUBBLE_R + 8) + b->w = 2 * BUBBLE_R + 8; + b->h = lines * LINE_H + 2 * BUBBLE_PAD_Y; + y += b->h + TIME_H + MSG_GAP; + } + c->content_h = y + CHAT_PAD - MSG_GAP; +} + +/* Fit the viewport between header and composer, keeping the offset */ +static void fit_viewport(sms_chat_t *c) +{ + c->scroll.flinging = false; + ui_scroll_setup(&c->scroll, chat_area(c), c->content_h); +} + +static void chat_layout(ui_view_t *v) +{ + fit_viewport((sms_chat_t *) v); +} + +/* Same, then bring the newest message into view */ +static void layout_viewport(sms_chat_t *c) +{ + c->gliding = false; + fit_viewport(c); + c->scroll.offset = ui_scroll_max(&c->scroll); +} + +/* + * Chat painting + */ +static void paint_bubble(sms_chat_t *c, twin_pixmap_t *px, int i) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = bubble_rect(c, i); + bool sent = msg_sent(c, i); + twin_argb32_t fill = sent ? th->sms_sent : th->sms_recv; + + ui_rrect(px, r, BUBBLE_R, fill); + /* Tighter corner on the speaker's side, pointing at the sender */ + ui_rect_t tail = ui_rect(sent ? r.right - BUBBLE_TAIL : r.left, + r.bottom - BUBBLE_TAIL, BUBBLE_TAIL, BUBBLE_TAIL); + ui_rrect(px, tail, 3, fill); + + twin_coord_t base = r.top + BUBBLE_PAD_Y + (LINE_H + 10) / 2; + ui_text_wrap(px, FONT_NORMAL, r.left + BUBBLE_PAD_X, base, + BUBBLE_MAX_W - 2 * BUBBLE_PAD_X, LINE_H, th->text, + msg_text(c, i), NULL); + + twin_coord_t ty = r.bottom + TIME_H - 3; + if (sent) + ui_text(px, FONT_SM, r.right - 4, ty, UI_RIGHT, th->text_sec, + msg_time(c, i)); + else + ui_text(px, FONT_SM, r.left + 4, ty, UI_LEFT, th->text_sec, + msg_time(c, i)); +} + +static void paint_messages(sms_chat_t *c, twin_pixmap_t *px, ui_rect_t clip) +{ + ui_rect_t area = c->scroll.area; + /* Bubbles may straddle the viewport edge: keep them off header/composer */ + twin_rect_t saved = ui_clip_push(px, area); + ui_fill(px, area, phone_theme()->bg); + + twin_coord_t lo = clip.top > area.top ? clip.top : area.top; + twin_coord_t hi = clip.bottom < area.bottom ? clip.bottom : area.bottom; + for (int i = 0; i < msg_count(c); i++) { + ui_rect_t r = bubble_rect(c, i); + if (r.bottom + TIME_H <= lo) + continue; + if (r.top >= hi) + break; /* bubbles are laid out top to bottom */ + paint_bubble(c, px, i); + } + ui_scroll_bar(px, &c->scroll); + twin_pixmap_restore_clip(px, saved); +} + +static void paint_header(sms_chat_t *c, twin_pixmap_t *px) +{ + const phone_theme_t *th = phone_theme(); + const char *name = c->thread->contact_name; + ui_rect_t r = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); + ui_rect_t room = ui_header(px, r, NULL, NULL, NULL); + /* Avatar and name centered together in the title's room */ + twin_coord_t w = ui_text_width(FONT_HEADING, name); + if (w > ui_w(room) - 24) + w = ui_w(room) - 24; + twin_coord_t x = (room.left + room.right - w - 24) / 2; + ui_avatar(px, x + 10, PHONE_HEADER_H / 2, 10, name[0], + ui_avatar_color(name)); + /* ui_text_fit() keeps 4 px of padding on each side */ + ui_text_fit(px, FONT_HEADING, ui_rect(x + 20, 0, w + 8, PHONE_HEADER_H), + UI_LEFT, th->on_bar, name); +} + +/* + * ui_field() ellipsizes the end of long text; while composing the end is + * what matters, so hand it the longest tail that fits instead. + */ +static void paint_field(sms_chat_t *c, twin_pixmap_t *px) +{ + ui_rect_t r = field_rect(c); + twin_coord_t room = ui_w(r) - 20; + const char *s = c->field.text; + while (*s && ui_text_width(FONT_NORMAL, s) > room) + s++; + if (s == c->field.text) { + ui_field(px, r, &c->field, "Type a message..."); + return; + } + ui_field_t tail = c->field; + ui_field_set(&tail, s); + ui_field(px, r, &tail, NULL); +} + +static void paint_send(sms_chat_t *c, twin_pixmap_t *px) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = send_rect(c); + twin_argb32_t bg, fg; + if (!c->field.len) { + /* Disabled: fade toward the composer instead of going translucent */ + bg = ui_mix(th->navbar, th->primary, 80); + fg = ui_mix(bg, th->on_primary, 120); + } else { + bg = c->send_pressed ? th->primary_dk : th->primary; + fg = th->on_primary; + } + ui_rrect(px, r, FIELD_H / 2, bg); + ui_icon(px, TWIN_SYM_SEND, FONT_NORMAL, (r.left + r.right) / 2 - 1, + (r.top + r.bottom) / 2, fg); + c->send_enabled = c->field.len > 0; +} + +static void paint_composer(sms_chat_t *c, twin_pixmap_t *px) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t r = composer_rect(c); + ui_fill(px, r, ui_mix(th->navbar, th->bg, 110)); + ui_fill(px, ui_rect(0, r.top, PHONE_W, 1), th->separator); + paint_field(c, px); + paint_send(c, px); +} + +static void chat_paint(ui_view_t *v, twin_pixmap_t *px) +{ + sms_chat_t *c = (sms_chat_t *) v; + ui_rect_t clip = ui_view_clip(v); + + if (ui_intersects(clip, c->scroll.area)) + paint_messages(c, px, clip); + if (ui_intersects(clip, ui_rect(0, 0, PHONE_W, PHONE_HEADER_H))) + paint_header(c, px); + if (ui_intersects(clip, composer_rect(c))) + paint_composer(c, px); + if (c->kbd.visible && ui_intersects(clip, kbd_rect(c))) + ui_kbd_draw(px, &c->kbd, kbd_rect(c)); +} + +/* + * Chat behavior + */ +static void field_changed(sms_chat_t *c) +{ + ui_view_invalidate(&c->view, field_rect(c)); + if (c->send_enabled != (c->field.len > 0)) + ui_view_invalidate(&c->view, send_rect(c)); +} + +static void set_keyboard(sms_chat_t *c, bool visible) +{ + if (c->kbd.visible == visible) + return; + ui_kbd_init(&c->kbd); + c->kbd.visible = visible; + c->field.focused = visible; + c->send_pressed = false; + layout_viewport(c); + ui_view_invalidate_all(&c->view); +} + +static bool glide_step(ui_view_t *v, twin_fixed_t value) +{ + sms_chat_t *c = (sms_chat_t *) v; + if (!c->gliding) + return false; + ui_scroll_to(v, &c->scroll, ui_lerp(c->glide_from, c->glide_to, value)); + if (value >= TWIN_FIXED_ONE) + c->gliding = false; + return true; +} + +static void send_message(sms_chat_t *c) +{ + if (!c->field.len) + return; + if (c->n_extra == MAX_EXTRA) { + /* Full: forget the oldest message sent in this session */ + memmove(&c->extra[0], &c->extra[1], + sizeof(c->extra[0]) * (MAX_EXTRA - 1)); + c->n_extra--; + } + sent_msg_t *m = &c->extra[c->n_extra++]; + memcpy(m->text, c->field.text, sizeof(m->text)); + phone_format_time(m->time, sizeof(m->time)); + ui_field_set(&c->field, ""); + c->send_pressed = false; + + /* Keep the current offset, then glide down to reveal the new bubble */ + twin_coord_t from = c->scroll.offset; + layout_messages(c); + fit_viewport(c); + c->scroll.offset = from; + ui_view_invalidate_all(&c->view); + c->glide_from = from; + c->glide_to = ui_scroll_max(&c->scroll); + c->gliding = c->glide_to != from && + ui_view_tween(&c->view, 220, TwinEaseOutCubic, glide_step); + if (!c->gliding) + c->scroll.offset = c->glide_to; +} + +static void set_send_pressed(sms_chat_t *c, bool pressed) +{ + if (c->send_pressed == pressed) + return; + c->send_pressed = pressed; + ui_view_invalidate(&c->view, send_rect(c)); +} + +/* Route a press-tracking event to the keyboard; repaint on visual change */ +static void kbd_feed(sms_chat_t *c, const ui_event_t *e) +{ + int key = ui_kbd_event(&c->kbd, kbd_rect(c), e); + if (!ui_empty(c->kbd.dirty)) + ui_view_invalidate(&c->view, c->kbd.dirty); + if (key == UI_KEY_ENTER) + send_message(c); + else if (key != UI_KEY_NONE && ui_field_key(&c->field, key)) + field_changed(c); +} + +static bool in_kbd(const sms_chat_t *c, twin_coord_t x, twin_coord_t y) +{ + return c->kbd.visible && ui_hit(kbd_rect(c), x, y); +} + +static bool chat_event(ui_view_t *v, const ui_event_t *e) +{ + sms_chat_t *c = (sms_chat_t *) v; + + if (e->kind == UI_PRESS) + c->gliding = false; /* the finger takes over from the glide */ + if (ui_scroll_event(v, &c->scroll, e)) { + set_send_pressed(c, false); + return true; + } + + switch (e->kind) { + case UI_PRESS: + if (in_kbd(c, e->x, e->y)) + kbd_feed(c, e); + else if (c->field.len && ui_hit(send_rect(c), e->x, e->y)) + set_send_pressed(c, true); + return true; + case UI_MOVE: + case UI_DRAG: + if (in_kbd(c, e->x0, e->y0)) + kbd_feed(c, e); + else + set_send_pressed( + c, c->send_pressed && ui_hit(send_rect(c), e->x, e->y)); + return true; + case UI_TAP: + if (in_kbd(c, e->x0, e->y0)) { + kbd_feed(c, e); + } else if (ui_hit(send_rect(c), e->x, e->y)) { + /* Disabled while empty: send_message() ignores an empty field */ + if (c->send_pressed) + send_message(c); + } else if (ui_hit(field_rect(c), e->x, e->y)) { + set_keyboard(c, true); + } else if (e->y < composer_top(c)) { + set_keyboard(c, false); /* tap outside the composer */ + } + set_send_pressed(c, false); + return true; + case UI_RELEASE: + case UI_CANCEL: + if (c->kbd.visible) + kbd_feed(c, e); + set_send_pressed(c, false); + return true; + case UI_KEY: { + if (e->ucs4 == UI_KEY_ENTER) { + send_message(c); + return true; + } + if (ui_field_key(&c->field, (int) e->ucs4)) { + field_changed(c); + return true; + } + return false; + } + default: + return false; + } +} + +static void chat_open(const phone_sms_thread_t *thread, int app) +{ + sms_chat_t *c = ui_view_new(sizeof(*c), thread->contact_name, chat_paint, + chat_event, NULL); + if (!c) + return; + c->view.app = app; + c->thread = thread; + c->stock = (int) thread->msg_count; + if (c->stock > MAX_MSGS - MAX_EXTRA) + c->stock = MAX_MSGS - MAX_EXTRA; + ui_kbd_init(&c->kbd); + layout_messages(c); + layout_viewport(c); + ui_view_set_layout(&c->view, chat_layout); + ui_view_show(&c->view); +} + +/* + * Conversation list + */ +static void list_layout(ui_view_t *v) +{ + sms_list_t *l = (sms_list_t *) v; + ui_scroll_setup( + &l->scroll, + ui_rect(0, PHONE_HEADER_H, PHONE_W, ui_view_height(v) - PHONE_HEADER_H), + PHONE_SMS_THREADS * LIST_ROW_H); +} + +static ui_rect_t row_rect(const sms_list_t *l, int i) +{ + return ui_rect(0, PHONE_HEADER_H + i * LIST_ROW_H - l->scroll.offset, + PHONE_W, LIST_ROW_H); +} + +static void paint_row(const sms_list_t *l, twin_pixmap_t *px, int i) +{ + const phone_theme_t *th = phone_theme(); + const phone_sms_thread_t *t = &phone_sms_threads()[i]; + ui_rect_t r = row_rect(l, i); + twin_coord_t cy = (r.top + r.bottom) / 2; + twin_coord_t x = 12 + 2 * LIST_AVATAR_R + 10; + + ui_fill(px, r, + i == l->pressed ? ui_mix(th->card, th->primary, 70) : th->card); + ui_avatar(px, 12 + LIST_AVATAR_R, cy, LIST_AVATAR_R, t->contact_name[0], + ui_avatar_color(t->contact_name)); + + twin_coord_t tw = ui_text_width(FONT_SM, t->timestamp); + ui_text(px, FONT_SM, PHONE_W - 10, r.top + 21, UI_RIGHT, th->text_sec, + t->timestamp); + /* ui_text_fit pads 4 px on each side; shift the box to align at x */ + ui_text_fit(px, FONT_NORMAL, + ui_rect(x - 4, r.top + 8, PHONE_W - 10 - tw - 4 - (x - 4), 18), + UI_LEFT, th->text, t->contact_name); + ui_text_fit(px, FONT_SM, ui_rect(x - 4, r.top + 27, PHONE_W - 6 - x, 16), + UI_LEFT, th->text_sec, t->preview); + ui_separator(px, ui_rect(x, r.top, PHONE_W - x, LIST_ROW_H)); +} + +static void list_paint(ui_view_t *v, twin_pixmap_t *px) +{ + sms_list_t *l = (sms_list_t *) v; + ui_rect_t clip = ui_view_clip(v); + ui_rect_t area = l->scroll.area; + + if (ui_intersects(clip, area)) { + twin_rect_t saved = ui_clip_push(px, area); + ui_fill(px, area, phone_theme()->bg); + for (int i = 0; i < PHONE_SMS_THREADS; i++) + if (ui_intersects(row_rect(l, i), clip)) + paint_row(l, px, i); + ui_scroll_bar(px, &l->scroll); + twin_pixmap_restore_clip(px, saved); + } + if (ui_intersects(clip, ui_rect(0, 0, PHONE_W, PHONE_HEADER_H))) + ui_header(px, ui_rect(0, 0, PHONE_W, PHONE_HEADER_H), NULL, "Messages", + NULL); +} + +static int row_at(const sms_list_t *l, twin_coord_t x, twin_coord_t y) +{ + if (!ui_hit(l->scroll.area, x, y)) + return -1; + for (int i = 0; i < PHONE_SMS_THREADS; i++) + if (ui_hit(row_rect(l, i), x, y)) + return i; + return -1; +} + +static ui_rect_t list_row(ui_view_t *v, int i) +{ + return row_rect((sms_list_t *) v, i); +} + +static void list_set_pressed(sms_list_t *l, int i) +{ + ui_view_set_pressed(&l->view, &l->pressed, i, list_row); +} + +static bool list_event(ui_view_t *v, const ui_event_t *e) +{ + sms_list_t *l = (sms_list_t *) v; + if (ui_scroll_event(v, &l->scroll, e)) { + list_set_pressed(l, -1); + return true; + } + switch (e->kind) { + case UI_PRESS: + list_set_pressed(l, row_at(l, e->x, e->y)); + return true; + case UI_MOVE: + if (row_at(l, e->x, e->y) != l->pressed) + list_set_pressed(l, -1); + return true; + case UI_TAP: { + int i = row_at(l, e->x, e->y); + list_set_pressed(l, -1); + if (i >= 0) + chat_open(&phone_sms_threads()[i], v->app); + return true; + } + case UI_DRAG: + case UI_RELEASE: + case UI_CANCEL: + list_set_pressed(l, -1); + return true; + default: + return false; + } +} + +void app_sms_open(void) +{ + sms_list_t *l = + ui_view_new(sizeof(*l), "Messages", list_paint, list_event, NULL); + if (!l) + return; + l->pressed = -1; + ui_view_set_layout(&l->view, list_layout); + ui_view_show(&l->view); +} + +/* + * Test hooks: state of the chat on top, or -1 / NULL when none + */ +static sms_chat_t *top_chat(void) +{ + ui_view_t *v = phone_top(); + return v && v->paint == chat_paint ? (sms_chat_t *) v : NULL; +} + +int app_sms_test_offset(void) +{ + sms_chat_t *c = top_chat(); + return c ? c->scroll.offset : -1; +} + +int app_sms_test_max_offset(void) +{ + sms_chat_t *c = top_chat(); + return c ? ui_scroll_max(&c->scroll) : -1; +} + +int app_sms_test_messages(void) +{ + sms_chat_t *c = top_chat(); + return c ? msg_count(c) : -1; +} + +int app_sms_test_keyboard(void) +{ + sms_chat_t *c = top_chat(); + return c ? c->kbd.visible : -1; +} + +const char *app_sms_test_field(void) +{ + sms_chat_t *c = top_chat(); + return c ? c->field.text : NULL; +} + +/* Text of message @i in the chat on top */ +const char *app_sms_test_message(int i) +{ + sms_chat_t *c = top_chat(); + return c && i >= 0 && i < msg_count(c) ? msg_text(c, i) : NULL; +} diff --git a/apps/cellphone/snake.c b/apps/cellphone/snake.c new file mode 100644 index 00000000..dc300459 --- /dev/null +++ b/apps/cellphone/snake.c @@ -0,0 +1,190 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* Arcade: Snake (see games.c) */ + +#include + +#include "games-internal.h" + +/* + * Snake: steer by breadth-first search to the fruit, but only along moves + * that leave at least the body's length of open cells reachable; failing + * that, take the move with the most room. + */ + +static const int8_t dirs[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + +static void snake_place_fruit(game_t *g) +{ + snake_t *s = &g->u.snake; + int free_cells = SN_N - s->len; + s->fx = -1; + if (free_cells <= 0) + return; + int k = game_rnd_below(g, free_cells); + for (int r = 0; r < SN_ROWS; r++) + for (int c = 0; c < SN_COLS; c++) + if (!s->occ[r][c] && k-- == 0) { + s->fx = c; + s->fy = r; + game_mark_cell(g, c, r); + return; + } +} + +static void snake_init(game_t *g) +{ + snake_t *s = &g->u.snake; + memset(s, 0, sizeof(*s)); + s->len = SN_START; + s->dx = 1; + for (int i = 0; i < SN_START; i++) { + s->head = i; + s->bx[i] = (uint8_t) (SN_COLS / 2 - SN_START + 1 + i); + s->by[i] = SN_ROWS / 2; + s->occ[s->by[i]][s->bx[i]] = 1; + } + snake_place_fruit(g); +} + +static int snake_tail(const snake_t *s) +{ + return (s->head - s->len + 1 + SN_N) % SN_N; +} + +/* Flood fill from (c, r) with @blocked; returns the reachable cell count + * and stores the distance to the fruit (-1 if unreachable) in *dist */ +static int snake_flood(const snake_t *s, + uint8_t blocked[SN_ROWS][SN_COLS], + int c, + int r, + int *dist) +{ + int16_t queue[SN_N]; + int16_t depth[SN_N]; + int n = 0, head = 0; + *dist = -1; + queue[n] = (int16_t) (r * SN_COLS + c); + depth[n++] = 0; + blocked[r][c] = 2; + while (head < n) { + int at = queue[head], d = depth[head++]; + int x = at % SN_COLS, y = at / SN_COLS; + if (x == s->fx && y == s->fy && *dist < 0) + *dist = d; + for (int i = 0; i < 4; i++) { + int nx = x + dirs[i][0], ny = y + dirs[i][1]; + if (nx < 0 || ny < 0 || nx >= SN_COLS || ny >= SN_ROWS || + blocked[ny][nx]) + continue; + blocked[ny][nx] = 2; + queue[n] = (int16_t) (ny * SN_COLS + nx); + depth[n++] = (int16_t) (d + 1); + } + } + return n; +} + +static void snake_tick(game_t *g) +{ + snake_t *s = &g->u.snake; + int hx = s->bx[s->head], hy = s->by[s->head]; + int tail = snake_tail(s); + int best = -1, best_key = 0; + + /* Try the current heading first so ties keep the snake going straight */ + int first = 0; + for (int i = 0; i < 4; i++) + if (dirs[i][0] == s->dx && dirs[i][1] == s->dy) + first = i; + for (int k = 0; k < 4; k++) { + int i = (first + k) % 4; + int nx = hx + dirs[i][0], ny = hy + dirs[i][1]; + if (nx < 0 || ny < 0 || nx >= SN_COLS || ny >= SN_ROWS) + continue; + bool eat = nx == s->fx && ny == s->fy; + bool frees_tail = !eat && nx == s->bx[tail] && ny == s->by[tail]; + if (s->occ[ny][nx] && !frees_tail) + continue; + uint8_t blocked[SN_ROWS][SN_COLS]; + memcpy(blocked, s->occ, sizeof(blocked)); + if (!eat) + blocked[s->by[tail]][s->bx[tail]] = 0; + int dist; + int room = snake_flood(s, blocked, nx, ny, &dist); + int key = (dist >= 0 && room >= s->len) ? dist : 10000 - room; + if (best < 0 || key < best_key) { + best = i; + best_key = key; + } + } + if (best < 0) { + game_restart(g); + return; + } + + s->dx = dirs[best][0]; + s->dy = dirs[best][1]; + int nx = hx + s->dx, ny = hy + s->dy; + bool eat = nx == s->fx && ny == s->fy; + if (!eat) { + s->occ[s->by[tail]][s->bx[tail]] = 0; + game_mark_cell(g, s->bx[tail], s->by[tail]); + } else { + s->len++; + } + s->head = (s->head + 1) % SN_N; + s->bx[s->head] = (uint8_t) nx; + s->by[s->head] = (uint8_t) ny; + s->occ[ny][nx] = 1; + game_mark_cell(g, hx, hy); /* old head turns into body */ + game_mark_cell(g, nx, ny); + if (eat) { + g->events++; + game_set_score(g, g->score + 1); + snake_place_fruit(g); + if (s->fx < 0) /* board filled: a win, start over */ + game_restart(g); + } +} + +static void snake_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) +{ + const snake_t *s = &g->u.snake; + int c0, c1, r0, r1; + int hx = s->bx[s->head], hy = s->by[s->head]; + game_cell_span(g, clip, &c0, &c1, &r0, &r1); + for (int r = r0; r <= r1; r++) { + for (int c = c0; c <= c1; c++) { + ui_rect_t cr = game_cell_rect(g, c, r); + ui_fill(px, cr, ((c + r) & 1) ? C_CELL_A : C_CELL_B); + if (c == s->fx && r == s->fy) { + ui_circle(px, (cr.left + cr.right) / 2, + (cr.top + cr.bottom) / 2, SN_CELL / 2 - 1, + UI_RGB(0xe53935)); + ui_circle(px, cr.left + 5, cr.top + 4, 1, + ui_alpha(UI_RGB(0xffffff), 0x90)); + } else if (c == hx && r == hy) { + ui_rrect(px, ui_inset(cr, 1), 4, UI_RGB(0xc5e1a5)); + /* Eyes look the way the snake is heading */ + int ex = (cr.left + cr.right) / 2 + s->dx * 3; + int ey = (cr.top + cr.bottom) / 2 + s->dy * 3; + ui_circle(px, ex - s->dy * 3, ey - s->dx * 3, 1, + UI_RGB(0x1b3a1d)); + ui_circle(px, ex + s->dy * 3, ey + s->dx * 3, 1, + UI_RGB(0x1b3a1d)); + } else if (s->occ[r][c]) { + ui_rrect(px, ui_inset(cr, 1), 3, UI_RGB(0x66bb6a)); + } + } + } +} + +const game_ops_t game_snake_ops = { + "Snake", 130, SN_COLS, SN_ROWS, SN_CELL, + INNER_X, INNER_Y + 1, snake_init, snake_tick, snake_paint, +}; diff --git a/apps/cellphone/tetris.c b/apps/cellphone/tetris.c new file mode 100644 index 00000000..2879fabf --- /dev/null +++ b/apps/cellphone/tetris.c @@ -0,0 +1,279 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* Arcade: Tetris (see games.c) */ + +#include + +#include "games-internal.h" + +/* + * Tetris: on spawn, score every rotation and column by the resting + * position's cleared lines, aggregate height, holes and bumpiness, then + * steer there one move per tick while the piece falls. + */ + +static const twin_argb32_t tt_colors[7] = { + UI_RGB(0x4dd0e1), UI_RGB(0xffd54f), UI_RGB(0xba68c8), UI_RGB(0x81c784), + UI_RGB(0xe57373), UI_RGB(0xffb74d), UI_RGB(0x7986cb), +}; + +/* 4x4 bitmasks, row-major from the top bit: I O T S Z L J */ +static const uint16_t tt_pieces[7][4] = { + {0x0F00, 0x2222, 0x00F0, 0x4444}, {0x6600, 0x6600, 0x6600, 0x6600}, + {0x0E40, 0x4C40, 0x4E00, 0x4640}, {0x06C0, 0x8C40, 0x6C00, 0x4620}, + {0x0C60, 0x4C80, 0xC600, 0x2640}, {0x0E80, 0xC440, 0x2E00, 0x44C0}, + {0x0E20, 0x44C0, 0x8E00, 0xC880}, +}; + +static bool tt_bit(int piece, int rot, int r, int c) +{ + return (tt_pieces[piece][rot] >> (15 - (r * 4 + c))) & 1; +} + +static bool tt_collides(const tetris_t *t, int piece, int rot, int x, int y) +{ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) { + if (!tt_bit(piece, rot, r, c)) + continue; + int fx = x + c, fy = y + r; + if (fx < 0 || fx >= TT_COLS || fy >= TT_ROWS) + return true; + if (fy >= 0 && t->field[fy][fx]) + return true; + } + return false; +} + +static void tt_stamp(tetris_t *t, int piece, int rot, int x, int y, uint8_t v) +{ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (tt_bit(piece, rot, r, c) && y + r >= 0) + t->field[y + r][x + c] = v; +} + +static int tt_full_rows(const tetris_t *t) +{ + int n = 0; + for (int r = 0; r < TT_ROWS; r++) { + int c = 0; + while (c < TT_COLS && t->field[r][c]) + c++; + n += c == TT_COLS; + } + return n; +} + +static int tt_evaluate(const tetris_t *t) +{ + int heights = 0, holes = 0, bumps = 0, prev = -1; + for (int c = 0; c < TT_COLS; c++) { + int h = 0; + for (int r = 0; r < TT_ROWS; r++) { + if (t->field[r][c]) { + if (!h) + h = TT_ROWS - r; + } else if (h) { + holes++; + } + } + heights += h; + if (prev >= 0) + bumps += h > prev ? h - prev : prev - h; + prev = h; + } + /* Weights after the well-known four-feature heuristic, scaled */ + return tt_full_rows(t) * 76 - heights * 51 - holes * 36 - bumps * 18; +} + +static void tt_choose(tetris_t *t) +{ + int best = 0; + bool found = false; + t->target_x = t->x; + t->target_rot = t->rot; + for (int rot = 0; rot < 4; rot++) { + for (int x = -2; x < TT_COLS; x++) { + if (tt_collides(t, t->piece, rot, x, t->y)) + continue; + int y = t->y; + while (!tt_collides(t, t->piece, rot, x, y + 1)) + y++; + tt_stamp(t, t->piece, rot, x, y, 1); + int score = tt_evaluate(t); + tt_stamp(t, t->piece, rot, x, y, 0); + if (!found || score > best) { + found = true; + best = score; + t->target_x = x; + t->target_rot = rot; + } + } + } +} + +static ui_rect_t tt_preview_rect(void) +{ + ui_rect_t in = game_inner_rect(); + twin_coord_t cx = (TT_SIDE_X + in.right) / 2; + return ui_rect(cx - 2 * TT_PREV_CELL - 4, TT_Y + 22, 4 * TT_PREV_CELL + 8, + 4 * TT_PREV_CELL + 8); +} + +/* Next piece in; returns false when it cannot enter (game over) */ +static bool tt_spawn(game_t *g) +{ + tetris_t *t = &g->u.tetris; + t->piece = t->next; + t->next = game_rnd_below(g, 7); + t->rot = 0; + t->x = TT_COLS / 2 - 2; + t->y = -1; + game_mark(g, tt_preview_rect()); + if (tt_collides(t, t->piece, t->rot, t->x, t->y)) + return false; + tt_choose(t); + return true; +} + +static void tetris_init(game_t *g) +{ + tetris_t *t = &g->u.tetris; + memset(t, 0, sizeof(*t)); + t->next = game_rnd_below(g, 7); + tt_spawn(g); +} + +/* Damage the cells of @piece at (x, y); call after the state changed so + * a batch painted early by game_mark() never shows a stale position */ +static void tt_mark_piece(game_t *g, int piece, int rot, int x, int y) +{ + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (tt_bit(piece, rot, r, c)) + game_mark_cell(g, x + c, y + r); +} + +static void tetris_tick(game_t *g) +{ + tetris_t *t = &g->u.tetris; + int piece = t->piece, rot = t->rot, x = t->x, y = t->y; + if (t->rot != t->target_rot && + !tt_collides(t, t->piece, (t->rot + 1) % 4, t->x, t->y)) + t->rot = (t->rot + 1) % 4; + int step = t->x < t->target_x ? 1 : t->x > t->target_x ? -1 : 0; + if (step && !tt_collides(t, t->piece, t->rot, t->x + step, t->y)) + t->x += step; + /* Soft drop once lined up: two rows per step */ + int fall = t->rot == t->target_rot && t->x == t->target_x ? 2 : 1; + while (fall-- && !tt_collides(t, t->piece, t->rot, t->x, t->y + 1)) + t->y++; + bool landed = t->y == y; /* rested a step without falling */ + tt_mark_piece(g, piece, rot, x, y); + tt_mark_piece(g, t->piece, t->rot, t->x, t->y); + if (!landed) + return; + + /* Landed: lock it in, clear full rows, bring in the next piece */ + tt_stamp(t, t->piece, t->rot, t->x, t->y, (uint8_t) (t->piece + 1)); + g->events++; + int cleared = 0; + for (int r = TT_ROWS - 1; r >= 0; r--) { + int c = 0; + while (c < TT_COLS && t->field[r][c]) + c++; + if (c < TT_COLS) + continue; + memmove(t->field[1], t->field[0], (size_t) r * TT_COLS); + memset(t->field[0], 0, TT_COLS); + cleared++; + r++; + } + if (!tt_spawn(g)) { + game_restart(g); + return; + } + if (cleared) { + game_set_score(g, g->score + cleared); + g->full++; + game_mark(g, game_inner_rect()); + } else { + tt_mark_piece(g, t->piece, t->rot, t->x, t->y); + } +} + +static void tt_cell(twin_pixmap_t *px, ui_rect_t r, twin_argb32_t color) +{ + ui_rrect(px, ui_inset(r, 1), 2, color); + /* Light top edge gives the blocks a little depth */ + ui_fill(px, ui_rect(r.left + 3, r.top + 2, ui_w(r) - 6, 1), + ui_alpha(UI_RGB(0xffffff), 0x60)); +} + +static void tetris_paint(game_t *g, twin_pixmap_t *px, ui_rect_t clip) +{ + const tetris_t *t = &g->u.tetris; + ui_rect_t well = ui_rect(TT_X, TT_Y, TT_COLS * TT_CELL, TT_ROWS * TT_CELL); + if (!ui_rect_contains(well, clip) && + ui_intersects(clip, ui_inset(well, -2))) + ui_rrect_stroke(px, ui_inset(well, -2), 3, 1, C_DIM); + + int c0, c1, r0, r1; + game_cell_span(g, clip, &c0, &c1, &r0, &r1); + for (int r = r0; r <= r1; r++) { + for (int c = c0; c <= c1; c++) { + ui_rect_t cr = game_cell_rect(g, c, r); + int pr = r - t->y, pc = c - t->x; + bool in_piece = pr >= 0 && pr < 4 && pc >= 0 && pc < 4 && + tt_bit(t->piece, t->rot, pr, pc); + if (in_piece) + tt_cell(px, cr, tt_colors[t->piece]); + else if (t->field[r][c]) + tt_cell(px, cr, tt_colors[t->field[r][c] - 1]); + else + ui_fill(px, ui_inset(cr, 1), C_CELL_B); + } + } + + /* Side column: next piece */ + ui_rect_t in = game_inner_rect(); + twin_coord_t cx = (TT_SIDE_X + in.right) / 2; + ui_rect_t label = ui_rect(TT_SIDE_X, TT_Y, in.right - TT_SIDE_X, 18); + if (ui_intersects(clip, label)) + ui_text_in(px, FONT_SM, label, UI_CENTER, C_LABEL, "Next"); + ui_rect_t pv = tt_preview_rect(); + if (ui_intersects(clip, pv)) { + ui_rrect(px, pv, 6, C_CELL_A); + int minr = 4, maxr = -1, minc = 4, maxc = -1; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + if (tt_bit(t->next, 0, r, c)) { + minr = r < minr ? r : minr; + maxr = r > maxr ? r : maxr; + minc = c < minc ? c : minc; + maxc = c > maxc ? c : maxc; + } + /* Center the piece's bounding box in the preview */ + int w = (maxc - minc + 1) * TT_PREV_CELL; + int h = (maxr - minr + 1) * TT_PREV_CELL; + int x0 = cx - w / 2, y0 = (pv.top + pv.bottom) / 2 - h / 2; + for (int r = minr; r <= maxr; r++) + for (int c = minc; c <= maxc; c++) + if (tt_bit(t->next, 0, r, c)) + tt_cell(px, + ui_rect(x0 + (c - minc) * TT_PREV_CELL, + y0 + (r - minr) * TT_PREV_CELL, + TT_PREV_CELL, TT_PREV_CELL), + tt_colors[t->next]); + } +} + +const game_ops_t game_tetris_ops = { + "Tetris", 110, TT_COLS, TT_ROWS, TT_CELL, + TT_X, TT_Y, tetris_init, tetris_tick, tetris_paint, +}; diff --git a/apps/cellphone/ui-input.c b/apps/cellphone/ui-input.c new file mode 100644 index 00000000..a0d4c53d --- /dev/null +++ b/apps/cellphone/ui-input.c @@ -0,0 +1,257 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone UI kit: text field and soft keyboard. + */ + +#include + +#include "ui-internal.h" + +/* + * Text field + */ +void ui_field(twin_pixmap_t *px, + ui_rect_t r, + const ui_field_t *f, + const char *placeholder) +{ + const phone_theme_t *th = phone_theme(); + ui_rrect(px, r, 6, th->card); + ui_rrect_stroke(px, r, 6, 1, f->focused ? th->primary : th->separator); + ui_rect_t t = ui_rect(r.left + 4, r.top, ui_w(r) - 8, ui_h(r)); + if (f->len) + ui_text_fit(px, FONT_NORMAL, t, UI_LEFT, th->text, f->text); + else if (placeholder) + /* Leave room for the caret in front of the placeholder */ + ui_text_in( + px, FONT_NORMAL, + ui_rect(t.left + (f->focused ? 3 : 0), t.top, ui_w(t), ui_h(t)), + UI_LEFT, th->text_sec, placeholder); + if (f->focused) { + twin_coord_t w = f->len ? ui_text_width(FONT_NORMAL, f->text) : 0; + twin_coord_t cx = min((twin_coord_t) (t.left + 4 + w + 1), + (twin_coord_t) (r.right - 6)); + ui_fill(px, ui_rect(cx, r.top + 6, 1, ui_h(r) - 12), th->primary); + } +} + +void ui_field_set(ui_field_t *f, const char *s) +{ + size_t n = strlen(s); + if (n >= sizeof(f->text)) + n = sizeof(f->text) - 1; + memcpy(f->text, s, n); + f->text[n] = 0; + f->len = (int) n; +} + +bool ui_field_key(ui_field_t *f, int key) +{ + if (key == UI_KEY_BACKSPACE) { + if (!f->len) + return false; + f->text[--f->len] = 0; + return true; + } + if (key >= 0x20 && key < 0x7f && f->len < (int) sizeof(f->text) - 1) { + f->text[f->len++] = (char) key; + f->text[f->len] = 0; + return true; + } + return false; +} + +/* + * Soft keyboard: four rows, letters or digits/symbols + */ +typedef struct { + ui_rect_t r; + int code; + const char *label; +} kbd_key_t; + +#define KBD_MAX_KEYS 40 + +static const char *kbd_rows_alpha[3] = {"qwertyuiop", "asdfghjkl", "zxcvbnm"}; +static const char *kbd_rows_sym[3] = {"1234567890", "-/:;()$&@", ".,?!'\"#"}; + +static int _kbd_layout(const ui_kbd_t *k, ui_rect_t r, kbd_key_t *keys) +{ + static char labels[KBD_MAX_KEYS][2]; + const char **rows = k->symbols ? kbd_rows_sym : kbd_rows_alpha; + int n = 0; + twin_coord_t kw = (ui_w(r) - 4) / 10, kh = (ui_h(r) - 10) / 4; + for (int row = 0; row < 3; row++) { + const char *keysrow = rows[row]; + int count = (int) strlen(keysrow); + twin_coord_t y = r.top + 4 + row * (kh + 2); + twin_coord_t x = r.left + 2 + (10 - count) * kw / 2; + if (row == 2) + x = r.left + 2 + kw * 3 / 2; + for (int i = 0; i < count; i++) { + char c = keysrow[i]; + if (k->shift && c >= 'a' && c <= 'z') + c = (char) (c - 'a' + 'A'); + labels[n][0] = c; + labels[n][1] = 0; + keys[n] = + (kbd_key_t) {ui_rect(x + i * kw, y, kw - 2, kh), c, labels[n]}; + n++; + } + if (row == 2) { + twin_coord_t wide = kw * 3 / 2 - 2; + if (!k->symbols) + keys[n++] = (kbd_key_t) {ui_rect(r.left + 2, y, wide, kh), + UI_KEY_SHIFT, "^"}; + keys[n++] = (kbd_key_t) {ui_rect(r.right - 2 - wide, y, wide, kh), + UI_KEY_BACKSPACE, TWIN_SYM_BACKSPACE}; + } + } + twin_coord_t y = r.top + 4 + 3 * (kh + 2); + keys[n++] = (kbd_key_t) {ui_rect(r.left + 2, y, kw * 2 - 2, kh), + UI_KEY_MODE, k->symbols ? "ABC" : "?123"}; + keys[n++] = (kbd_key_t) {ui_rect(r.left + 2 + kw * 2, y, kw * 5 - 2, kh), + ' ', "space"}; + keys[n++] = + (kbd_key_t) {ui_rect(r.left + 2 + kw * 7, y, kw - 2, kh), '.', "."}; + keys[n++] = (kbd_key_t) {ui_rect(r.left + 2 + kw * 8, y, kw * 2 - 2, kh), + UI_KEY_ENTER, TWIN_SYM_SEND}; + return n; +} + +void ui_kbd_init(ui_kbd_t *k) +{ + memset(k, 0, sizeof(*k)); + k->pressed = -1; +} + +ui_rect_t ui_kbd_rect(twin_coord_t view_h) +{ + return ui_rect(0, view_h - UI_KBD_H, PHONE_W, UI_KBD_H); +} + +void ui_kbd_draw(twin_pixmap_t *px, const ui_kbd_t *k, ui_rect_t r) +{ + const phone_theme_t *th = phone_theme(); + kbd_key_t keys[KBD_MAX_KEYS]; + int n = _kbd_layout(k, r, keys); + ui_fill(px, r, ui_mix(th->bg, th->text_sec, 60)); + for (int i = 0; i < n; i++) { + bool special = keys[i].code >= 0x100 || keys[i].code == UI_KEY_ENTER || + keys[i].code == UI_KEY_BACKSPACE; + twin_argb32_t bg = + special ? ui_mix(th->card, th->text_sec, 60) : th->card; + if (keys[i].code == UI_KEY_SHIFT && k->shift) + bg = th->primary; + if (i == k->pressed) + bg = ui_mix(bg, th->primary, 120); + /* 1 px pressed travel for tactile feedback */ + ui_rect_t kr = keys[i].r; + if (i == k->pressed) { + kr.top++; + kr.bottom++; + } + ui_rrect(px, kr, 4, bg); + int size = keys[i].code == ' ' || keys[i].code >= 0x100 ? FONT_SM + : FONT_NORMAL; + if (_is_symbol(keys[i].label)) + ui_icon(px, keys[i].label, FONT_NORMAL, (kr.left + kr.right) / 2, + (kr.top + kr.bottom) / 2, th->text); + else + ui_text_in(px, size, kr, UI_CENTER, th->text, keys[i].label); + } +} + +bool ui_kbd_find(const ui_kbd_t *k, ui_rect_t r, int code, ui_rect_t *out) +{ + kbd_key_t keys[KBD_MAX_KEYS]; + int n = _kbd_layout(k, r, keys); + for (int i = 0; i < n; i++) { + if (keys[i].code == code) { + *out = keys[i].r; + return true; + } + } + return false; +} + +/* Pressed keys draw 1 px lower: cover both positions */ +static ui_rect_t _key_dirty(const kbd_key_t *keys, int i) +{ + if (i < 0) + return (ui_rect_t) {0, 0, 0, 0}; + ui_rect_t r = keys[i].r; + r.bottom++; + return r; +} + +static int _kbd_event(ui_kbd_t *k, + const kbd_key_t *keys, + int n, + const ui_event_t *e); + +int ui_kbd_event(ui_kbd_t *k, ui_rect_t r, const ui_event_t *e) +{ + kbd_key_t keys[KBD_MAX_KEYS]; + int before = k->pressed; + bool shift = k->shift, symbols = k->symbols; + int n = _kbd_layout(k, r, keys); + int key = _kbd_event(k, keys, n, e); + if (shift != k->shift || symbols != k->symbols) + k->dirty = r; /* every label changed */ + else if (before != k->pressed) + k->dirty = ui_rect_union(_key_dirty(keys, before), + _key_dirty(keys, k->pressed)); + else + k->dirty = (ui_rect_t) {0, 0, 0, 0}; + return key; +} + +/* @keys is the layout as it was before the event */ +static int _kbd_event(ui_kbd_t *k, + const kbd_key_t *keys, + int n, + const ui_event_t *e) +{ + int hit = -1; + for (int i = 0; i < n; i++) + if (ui_hit(keys[i].r, e->x, e->y)) + hit = i; + + switch (e->kind) { + case UI_PRESS: + case UI_MOVE: + case UI_DRAG: + k->pressed = hit; + return UI_KEY_NONE; + case UI_TAP: { + k->pressed = -1; + if (hit < 0) + return UI_KEY_NONE; + int code = keys[hit].code; + if (code == UI_KEY_SHIFT) { + k->shift = !k->shift; + return code; + } + if (code == UI_KEY_MODE) { + k->symbols = !k->symbols; + k->shift = false; + return code; + } + if (code < 0x100) + k->shift = false; /* one-shot; the layout already uppercased */ + return code; + } + case UI_RELEASE: + case UI_CANCEL: + k->pressed = -1; + return UI_KEY_NONE; + default: + return UI_KEY_NONE; + } +} diff --git a/apps/cellphone/ui-internal.h b/apps/cellphone/ui-internal.h new file mode 100644 index 00000000..5a5e1033 --- /dev/null +++ b/apps/cellphone/ui-internal.h @@ -0,0 +1,29 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Helpers shared by the UI kit's own files (ui.c, ui-paint.c, ui-input.c); + * apps use phone.h only. + */ + +#ifndef _UI_INTERNAL_H_ +#define _UI_INTERNAL_H_ + +#include "phone.h" + +#ifndef min +#define min(a, b) ((a) < (b) ? (a) : (b)) +#define max(a, b) ((a) > (b) ? (a) : (b)) +#endif + +/* Font Awesome code points live in U+F000..U+F8FF: UTF-8 0xEF 0x80.. */ +static inline bool _is_symbol(const char *s) +{ + return s && (uint8_t) s[0] == 0xef && (uint8_t) s[1] >= 0x80 && + (uint8_t) s[1] <= 0xa3; +} + +#endif /* _UI_INTERNAL_H_ */ diff --git a/apps/cellphone/ui-paint.c b/apps/cellphone/ui-paint.c new file mode 100644 index 00000000..a8a8dba7 --- /dev/null +++ b/apps/cellphone/ui-paint.c @@ -0,0 +1,729 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone UI kit: colors, text, shapes and the widgets drawn from them. + */ + +#include + +#include "ui-internal.h" + +/* + * Colors + */ +twin_argb32_t ui_mix(twin_argb32_t a, twin_argb32_t b, int t) +{ + twin_argb32_t out = 0; + if (t <= 0) + return a; + if (t >= 255) + return b; + for (int s = 0; s < 32; s += 8) { + int ca = (a >> s) & 0xff, cb = (b >> s) & 0xff; + out |= (twin_argb32_t) ((ca + ((cb - ca) * t + 127) / 255) & 0xff) << s; + } + return out; +} + +twin_argb32_t ui_darken(twin_argb32_t c, int pct) +{ + return ui_mix(c, c & 0xff000000, pct * 255 / 100); +} + +twin_argb32_t ui_alpha(twin_argb32_t c, twin_a8_t a) +{ + twin_argb32_t out = 0; + for (int s = 0; s < 32; s += 8) + out |= (twin_argb32_t) ((((c >> s) & 0xff) * a + 127) / 255) << s; + return out; +} + +bool ui_intersects(ui_rect_t a, ui_rect_t b) +{ + return a.left < b.right && b.left < a.right && a.top < b.bottom && + b.top < a.bottom; +} + +/* + * True when [l, r) x [t, b), grown by 1 px for anti-aliasing, misses the + * clip: the shape would draw nothing, so skip building its path. + */ +static bool _culled(twin_pixmap_t *px, + twin_coord_t l, + twin_coord_t t, + twin_coord_t r, + twin_coord_t b) +{ + twin_rect_t clip = twin_pixmap_get_clip(px); + return r + 1 <= clip.left || l - 1 >= clip.right || b + 1 <= clip.top || + t - 1 >= clip.bottom; +} + +/* A line of @size text with its baseline at @y misses the clip vertically. + * The band is wider than twin_text_draw()'s own test, so skipping here never + * changes what is drawn. */ +static bool _text_culled(twin_pixmap_t *px, int size, twin_coord_t y) +{ + twin_rect_t clip = twin_pixmap_get_clip(px); + return y + size + 2 <= clip.top || y - 2 * size - 2 >= clip.bottom; +} + +/* + * Typography + */ +twin_coord_t ui_text_width(int size, const char *s) +{ + return twin_fixed_to_int( + twin_text_width(&twin_font_montserrat, ui_fx(size), s) + + TWIN_FIXED_HALF); +} + +void ui_text(twin_pixmap_t *px, + int size, + twin_coord_t x, + twin_coord_t y, + ui_align_t align, + twin_argb32_t color, + const char *s) +{ + if (_text_culled(px, size, y)) + return; + twin_fixed_t left = ui_fx(x); + if (align != UI_LEFT) { + twin_fixed_t w = twin_text_width(&twin_font_montserrat, ui_fx(size), s); + left -= align == UI_CENTER ? w / 2 : w; + } + twin_text_draw(px, &twin_font_montserrat, ui_fx(size), left, ui_fx(y), + color, s); +} + +int ui_text_wrap(twin_pixmap_t *px, + int size, + twin_coord_t x, + twin_coord_t y, + twin_coord_t width, + twin_coord_t line_h, + twin_argb32_t color, + const char *s, + twin_coord_t *widest) +{ + char line[128]; + int lines = 0; + twin_coord_t wmax = 0; + while (*s) { + while (*s == ' ') + s++; + if (!*s) + break; + /* Greedily take words while they fit */ + size_t n = 0, fit = 0; + while (n < sizeof(line) - 1 && s[n]) { + size_t end = n; + while (end < sizeof(line) - 1 && s[end] && s[end] != ' ') + end++; + memcpy(line, s, end); + line[end] = 0; + if (fit && ui_text_width(size, line) > width) + break; + fit = end; + n = end; + if (s[n] == ' ') + n++; + } + if (!fit) /* a single word wider than the line: hard break */ + fit = n ? n : 1; + memcpy(line, s, fit); + line[fit] = 0; + /* A single word wider than the line: break inside it, at a + * character boundary, keeping at least one character */ + while (ui_text_width(size, line) > width) { + size_t cut = fit - 1; + while (cut && ((uint8_t) s[cut] & 0xc0) == 0x80) + cut--; + if (!cut) + break; + fit = cut; + line[fit] = 0; + } + twin_coord_t w = ui_text_width(size, line); + if (w > wmax) + wmax = w; + if (px) + ui_text(px, size, x, y + lines * line_h, UI_LEFT, color, line); + lines++; + s += fit; + } + if (widest) + *widest = wmax; + return lines; +} + +twin_coord_t ui_baseline(int size, twin_coord_t top, twin_coord_t h) +{ + /* Center the cap height (~0.7 em) rather than the full line box */ + return top + (h + size * 7 / 10 + 1) / 2; +} + +void ui_text_in(twin_pixmap_t *px, + int size, + ui_rect_t r, + ui_align_t align, + twin_argb32_t color, + const char *s) +{ + twin_coord_t x = align == UI_LEFT ? r.left + 4 + : align == UI_RIGHT ? r.right - 4 + : (r.left + r.right) / 2; + ui_text(px, size, x, ui_baseline(size, r.top, ui_h(r)), align, color, s); +} + +void ui_text_fit(twin_pixmap_t *px, + int size, + ui_rect_t r, + ui_align_t align, + twin_argb32_t color, + const char *s) +{ + if (!*s) + return; /* also keeps buf below from being drawn uninitialized */ + twin_coord_t room = ui_w(r) - 8; + if (ui_text_width(size, s) <= room) { + ui_text_in(px, size, r, align, color, s); + return; + } + char buf[96] = "..."; + size_t n = strlen(s); + if (n > sizeof(buf) - 4) + n = sizeof(buf) - 4; + while (n > 0) { + /* Never cut a UTF-8 sequence: the partial glyph would end the + * string early and drop the ellipsis */ + while (n > 0 && ((unsigned char) s[n] & 0xc0) == 0x80) + n--; + if (!n) + break; + size_t m = n; + while (m > 0 && s[m - 1] == ' ') + m--; /* "how about..." rather than "how about ..." */ + memcpy(buf, s, m); + strcpy(buf + m, "..."); + if (ui_text_width(size, buf) <= room) + break; + n--; + } + ui_text_in(px, size, r, align, color, buf); +} + +void ui_icon(twin_pixmap_t *px, + const char *sym, + int size, + twin_coord_t cx, + twin_coord_t cy, + twin_argb32_t color) +{ + /* Font Awesome glyphs span -0.125 em .. 0.875 em around the baseline */ + if (_text_culled(px, size, cy + size * 3 / 8 + 1)) + return; + twin_fixed_t w = twin_text_width(&twin_font_icons, ui_fx(size), sym); + twin_fixed_t base = ui_fx(cy) + ui_fx(size) * 3 / 8; + twin_text_draw(px, &twin_font_icons, ui_fx(size), ui_fx(cx) - w / 2, base, + color, sym); +} + +/* + * Shapes + */ +static twin_path_t *shape_path; +static twin_path_t *user_path; + +/* Dot texture: 1 px dots on a 3 px pitch, inset so no dot touches the + * edge, drawn through one mask in a single composite */ +#define DOT_MASK 32 +static twin_pixmap_t *dot_mask; + +static twin_pixmap_t *_dot_mask_create(void) +{ + twin_pixmap_t *m = twin_pixmap_create(TWIN_A8, DOT_MASK, DOT_MASK); + if (!m) + return NULL; + twin_coord_t stride; + uint8_t *p = twin_pixmap_pixels(m, &stride); + for (twin_coord_t y = 0; y < DOT_MASK; y++) + for (twin_coord_t x = 0; x < DOT_MASK; x++) + p[y * stride + x] = (x % 3 == 2 && y % 3 == 2) ? 0xff : 0; + return m; +} + +bool ui_init(void) +{ + if (!shape_path) + shape_path = twin_path_create(); + if (!user_path) + user_path = twin_path_create(); + if (!dot_mask) + dot_mask = _dot_mask_create(); + if (shape_path && user_path && dot_mask) + return true; + ui_fini(); + return false; +} + +void ui_fini(void) +{ + if (shape_path) + twin_path_destroy(shape_path); + if (user_path) + twin_path_destroy(user_path); + if (dot_mask) + twin_pixmap_destroy(dot_mask); + shape_path = user_path = NULL; + dot_mask = NULL; +} + +/* Both paths exist from ui_init() on, so these never fail */ +static twin_path_t *_shape(void) +{ + twin_path_empty(shape_path); + return shape_path; +} + +twin_path_t *ui_path(void) +{ + twin_path_empty(user_path); + return user_path; +} + +void ui_path_done(twin_path_t *path) +{ + if (path) + twin_path_empty(path); +} + +void ui_fill(twin_pixmap_t *px, ui_rect_t r, twin_argb32_t color) +{ + twin_fill(px, color, (color >> 24) == 0xff ? TWIN_SOURCE : TWIN_OVER, + r.left, r.top, r.right, r.bottom); +} + +static void _rrect_path(twin_path_t *p, ui_rect_t r, int radius) +{ + int max_r = min(ui_w(r), ui_h(r)) / 2; + if (radius > max_r) + radius = max_r; + if (radius <= 0) + twin_path_rectangle(p, ui_fx(r.left), ui_fx(r.top), ui_fx(ui_w(r)), + ui_fx(ui_h(r))); + else + twin_path_rounded_rectangle(p, ui_fx(r.left), ui_fx(r.top), + ui_fx(ui_w(r)), ui_fx(ui_h(r)), + ui_fx(radius), ui_fx(radius)); +} + +void ui_rrect(twin_pixmap_t *px, ui_rect_t r, int radius, twin_argb32_t color) +{ + if (ui_w(r) <= 0 || ui_h(r) <= 0 || + _culled(px, r.left, r.top, r.right, r.bottom)) + return; + if (radius <= 0) { + ui_fill(px, r, color); + return; + } + twin_path_t *p = _shape(); + _rrect_path(p, r, radius); + twin_paint_path(px, color, p); +} + +void ui_rrect_grad(twin_pixmap_t *px, + ui_rect_t r, + int radius, + twin_argb32_t top, + twin_argb32_t bottom) +{ + if (ui_w(r) <= 0 || ui_h(r) <= 0 || + _culled(px, r.left, r.top, r.right, r.bottom)) + return; + if (top == bottom) { + ui_rrect(px, r, radius, top); + return; + } + twin_path_t *p = _shape(); + _rrect_path(p, r, radius); + twin_paint_path_linear(px, p, 0, ui_fx(r.top), top, 0, ui_fx(r.bottom), + bottom); +} + +void ui_rrect_stroke(twin_pixmap_t *px, + ui_rect_t r, + int radius, + int width, + twin_argb32_t color) +{ + if (_culled(px, r.left, r.top, r.right, r.bottom)) + return; + twin_path_t *p = _shape(); + /* Stroke centered on a rectangle inset by half the pen */ + twin_fixed_t h = ui_fx(width) / 2; + twin_fixed_t w = ui_fx(ui_w(r)) - 2 * h, hh = ui_fx(ui_h(r)) - 2 * h; + twin_fixed_t rad = ui_fx(radius) - h; + if (rad < 0) + rad = 0; + twin_path_rounded_rectangle(p, ui_fx(r.left) + h, ui_fx(r.top) + h, w, hh, + rad, rad); + twin_paint_stroke(px, color, p, ui_fx(width)); +} + +void ui_circle(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + twin_argb32_t color) +{ + if (_culled(px, cx - radius, cy - radius, cx + radius, cy + radius)) + return; + twin_path_t *p = _shape(); + twin_path_circle(p, ui_fx(cx), ui_fx(cy), ui_fx(radius)); + twin_paint_path(px, color, p); +} + +void ui_ring(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + int width, + twin_argb32_t color) +{ + if (_culled(px, cx - radius, cy - radius, cx + radius, cy + radius)) + return; + twin_path_t *p = _shape(); + twin_path_circle(p, ui_fx(cx), ui_fx(cy), ui_fx(radius) - ui_fx(width) / 2); + twin_paint_stroke(px, color, p, ui_fx(width)); +} + +void ui_line(twin_pixmap_t *px, + twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1, + int width, + twin_argb32_t color) +{ + if (_culled(px, min(x0, x1) - width, min(y0, y1) - width, + max(x0, x1) + width, max(y0, y1) + width)) + return; + twin_path_t *p = _shape(); + twin_path_move(p, ui_fx(x0), ui_fx(y0)); + twin_path_draw(p, ui_fx(x1), ui_fx(y1)); + twin_paint_stroke(px, color, p, ui_fx(width)); +} + +void ui_vgrad(twin_pixmap_t *px, + ui_rect_t r, + twin_argb32_t top, + twin_argb32_t bottom) +{ + twin_coord_t h = ui_h(r); + if (h <= 0) + return; + if (top == bottom) { + ui_fill(px, r, top); + return; + } + /* One solid span per row; rows outside the clip cost nothing */ + twin_rect_t clip = twin_pixmap_get_clip(px); + twin_coord_t y0 = max(r.top, clip.top), y1 = min(r.bottom, clip.bottom); + for (twin_coord_t y = y0; y < y1; y++) { + int t = h > 1 ? (y - r.top) * 255 / (h - 1) : 0; + twin_argb32_t c = ui_mix(top, bottom, t); + twin_fill(px, c, (c >> 24) == 0xff ? TWIN_SOURCE : TWIN_OVER, r.left, y, + r.right, y + 1); + } +} + +/* + * Widgets + */ +static void _dotgrid(twin_pixmap_t *px, ui_rect_t r, twin_argb32_t color) +{ + /* Dots stop one pixel short of the right and bottom edges */ + twin_coord_t w = ui_w(r) - 1, h = ui_h(r) - 1; + if (w <= 0 || h <= 0 || !dot_mask) + return; + twin_operand_t src = {.source_kind = TWIN_SOLID, .u.argb = color}; + twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = dot_mask}; + twin_composite(px, r.left, r.top, &src, 0, 0, &msk, 0, 0, TWIN_OVER, + w < DOT_MASK ? w : DOT_MASK, h < DOT_MASK ? h : DOT_MASK); +} + +ui_rect_t ui_header(twin_pixmap_t *px, + ui_rect_t r, + const char *lead, + const char *title, + const char *trailing) +{ + const phone_theme_t *th = phone_theme(); + const twin_coord_t slot = UI_HEADER_SLOT; + twin_argb32_t dots = ui_alpha(th->on_bar, 0x38); + twin_coord_t cy = (r.top + r.bottom) / 2; + /* Texture patch for an empty action slot, narrower than the slot so + * the title may use the rest */ + twin_coord_t tex_w = UI_HEADER_TEXTURE - 8, tex_top = r.top + 7, + tex_h = ui_h(r) - 14; + + ui_vgrad(px, r, th->header, th->header_grad); + ui_fill(px, ui_rect(r.left, r.bottom - 1, ui_w(r), 1), + ui_darken(th->header_grad, 25)); + + twin_coord_t start = r.left + (lead ? slot : UI_HEADER_TEXTURE); + twin_coord_t end = r.right - UI_HEADER_TEXTURE; + if (lead) + ui_icon(px, lead, FONT_HEADING, r.left + slot / 2, cy, th->on_bar); + else + _dotgrid(px, ui_rect(r.left + 6, tex_top, tex_w, tex_h), dots); + + if (trailing) { + twin_coord_t tw = ui_text_width(FONT_SM, trailing); + ui_text_in(px, FONT_SM, ui_rect(r.left, r.top, ui_w(r) - 8, ui_h(r)), + UI_RIGHT, ui_mix(th->on_bar, th->header, 70), trailing); + end = r.right - 8 - tw - 8; + } else { + _dotgrid(px, ui_rect(r.right - 6 - tex_w, tex_top, tex_w, tex_h), dots); + } + + ui_rect_t room = {start, end, r.top, r.bottom}; + if (!title || !title[0]) + return room; + twin_coord_t w = ui_text_width(FONT_HEADING, title); + twin_coord_t mid = (r.left + r.right) / 2; + if (mid - w / 2 >= start && mid + (w + 1) / 2 <= end) + ui_text_in(px, FONT_HEADING, r, UI_CENTER, th->on_bar, title); + else + ui_text_fit(px, FONT_HEADING, room, UI_LEFT, th->on_bar, title); + return room; +} + +void ui_section(twin_pixmap_t *px, ui_rect_t r, const char *label) +{ + const phone_theme_t *th = phone_theme(); + ui_fill(px, r, th->group); + ui_text_in(px, FONT_SM, ui_rect(r.left + 10, r.top, ui_w(r) - 10, ui_h(r)), + UI_LEFT, th->text_sec, label); +} + +static void _button_colors(ui_button_style_t style, + bool pressed, + twin_argb32_t *bg, + twin_argb32_t *fg) +{ + const phone_theme_t *th = phone_theme(); + switch (style) { + case UI_BTN_PRIMARY: + *bg = th->primary; + *fg = th->on_primary; + break; + case UI_BTN_GREEN: + *bg = th->call_green; + *fg = 0xffffffff; + break; + case UI_BTN_RED: + *bg = th->call_red; + *fg = 0xffffffff; + break; + case UI_BTN_FLAT: + *bg = pressed ? ui_mix(th->bg, th->primary, 70) : 0; + *fg = th->text; + return; + case UI_BTN_CARD: + default: + *bg = th->card; + *fg = th->text; + break; + } + if (pressed) + *bg = ui_darken(*bg, 18); +} + +void ui_button(twin_pixmap_t *px, + ui_rect_t r, + int radius, + ui_button_style_t style, + bool pressed, + int size, + const char *label) +{ + twin_argb32_t bg, fg; + _button_colors(style, pressed, &bg, &fg); + if (bg && style == UI_BTN_FLAT) { + ui_rrect(px, r, radius, bg); + } else if (bg) { + /* Soft fill: a faint ledge below, lit slightly from the top */ + if (!pressed) + ui_rrect(px, ui_rect(r.left, r.top + 1, ui_w(r), ui_h(r)), radius, + 0x18000000); + ui_rrect_grad(px, r, radius, ui_mix(bg, UI_WHITE, pressed ? 0 : 28), + bg); + } + if (!label) + return; + if (_is_symbol(label)) + ui_icon(px, label, size, (r.left + r.right) / 2, (r.top + r.bottom) / 2, + fg); + else + ui_text_in(px, size, r, UI_CENTER, fg, label); +} + +/* Muted, earthy tones in the launcher's family, white initials legible */ +static const twin_argb32_t avatar_palette[] = { + 0xff5a7896, 0xff5b8c45, 0xffb5843a, 0xffa95c72, + 0xff7a6a96, 0xff4f8c7c, 0xffad6a48, 0xff646c9a, +}; + +twin_argb32_t ui_avatar_color(const char *name) +{ + /* Keyed by initial: neighbors in an alphabetical list always differ */ + const unsigned n = sizeof(avatar_palette) / sizeof(avatar_palette[0]); + unsigned c = name ? (unsigned char) name[0] : 0; + if (c >= 'a' && c <= 'z') + c -= 'a' - 'A'; + if (c >= 'A' && c <= 'Z') + return avatar_palette[(c - 'A') % n]; + unsigned h = 0x811c9dc5u; + while (name && *name) + h = (h ^ (unsigned char) *name++) * 0x01000193u; + return avatar_palette[(h ^ (h >> 16)) % n]; +} + +void ui_avatar(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int radius, + char initial, + twin_argb32_t color) +{ + char s[2] = {initial, 0}; + ui_circle(px, cx, cy, radius, color); + int size = radius >= 20 ? FONT_LARGE : FONT_NORMAL; + ui_text(px, size, cx, ui_baseline(size, cy - radius, radius * 2), UI_CENTER, + 0xffffffff, s); +} + +void ui_list_row(twin_pixmap_t *px, + ui_rect_t r, + bool pressed, + const char *sym, + twin_argb32_t sym_color, + const char *title, + const char *subtitle, + const char *detail) +{ + const phone_theme_t *th = phone_theme(); + ui_fill(px, r, pressed ? ui_mix(th->card, th->primary, 70) : th->card); + twin_coord_t x = r.left + 10; + if (sym) { + twin_coord_t cy = (r.top + r.bottom) / 2; + if (_is_symbol(sym)) { + ui_icon(px, sym, FONT_HEADING, x + 10, cy, sym_color); + } else { + ui_avatar(px, x + 12, cy, 13, sym[0], sym_color); + } + x += 32; + } + twin_coord_t detail_w = detail ? ui_text_width(FONT_SM, detail) + 12 : 0; + ui_rect_t text = {x, (twin_coord_t) (r.right - detail_w - 4), r.top, + r.bottom}; + if (subtitle) { + ui_rect_t top = {text.left, text.right, r.top + 3, + (twin_coord_t) (r.top + ui_h(r) / 2 + 1)}; + ui_rect_t bot = {text.left, text.right, + (twin_coord_t) (r.top + ui_h(r) / 2 - 1), + r.bottom - 3}; + top.left -= 4; + bot.left -= 4; + ui_text_fit(px, FONT_NORMAL, top, UI_LEFT, th->text, title); + ui_text_fit(px, FONT_SM, bot, UI_LEFT, th->text_sec, subtitle); + } else { + text.left -= 4; + ui_text_fit(px, FONT_NORMAL, text, UI_LEFT, th->text, title); + } + if (detail) + ui_text_in(px, FONT_SM, ui_rect(r.left, r.top, ui_w(r) - 6, ui_h(r)), + UI_RIGHT, th->text_sec, detail); + ui_separator(px, ui_rect(x, r.top, r.right - x, ui_h(r))); +} + +twin_rect_t ui_clip_push(twin_pixmap_t *px, ui_rect_t r) +{ + twin_rect_t saved = twin_pixmap_save_clip(px); + twin_pixmap_clip(px, r.left, r.top, r.right, r.bottom); + return saved; +} + +void ui_separator(twin_pixmap_t *px, ui_rect_t r) +{ + ui_fill(px, ui_rect(r.left, r.bottom - 1, ui_w(r), 1), + phone_theme()->separator); +} + +void ui_slider(twin_pixmap_t *px, ui_rect_t r, int value, bool pressed) +{ + const phone_theme_t *th = phone_theme(); + twin_coord_t cy = (r.top + r.bottom) / 2; + twin_coord_t x0 = r.left + 8, x1 = r.right - 8; + twin_coord_t kx = ui_slider_knob_x(r, value); + ui_rrect(px, ui_rect(x0, cy - 2, x1 - x0, 4), 2, th->indicator); + ui_rrect(px, ui_rect(x0, cy - 2, kx - x0, 4), 2, th->primary); + int kr = pressed ? 9 : 7; + ui_circle(px, kx, cy + 1, kr + 1, 0x30000000); + ui_circle(px, kx, cy, kr, 0xffffffff); + ui_ring(px, kx, cy, kr, 1, ui_alpha(th->primary, 0xc0)); +} + +twin_coord_t ui_slider_knob_x(ui_rect_t r, int value) +{ + twin_coord_t x0 = r.left + 8, x1 = r.right - 8; + return x0 + (x1 - x0) * value / 1000; +} + +int ui_slider_value_at(ui_rect_t r, twin_coord_t x) +{ + twin_coord_t x0 = r.left + 8, x1 = r.right - 8; + if (x <= x0) + return 0; + if (x >= x1) + return 1000; + return (x - x0) * 1000 / (x1 - x0); +} + +void ui_switch(twin_pixmap_t *px, ui_rect_t r, bool on) +{ + const phone_theme_t *th = phone_theme(); + ui_rect_t track = + ui_rect(r.right - 38, (r.top + r.bottom) / 2 - 10, 36, 20); + ui_rrect(px, track, 10, on ? th->primary : th->indicator); + twin_coord_t kx = on ? track.right - 10 : track.left + 10; + ui_circle(px, kx, (track.top + track.bottom) / 2, 8, 0xffffffff); +} + +void ui_dots(twin_pixmap_t *px, + twin_coord_t cx, + twin_coord_t cy, + int count, + int current) +{ + const phone_theme_t *th = phone_theme(); + int total = + count * 10 + 4; /* dots, the wider current one, no trailing gap */ + twin_coord_t x = cx - total / 2; + for (int i = 0; i < count; i++) { + if (i == current) { + ui_rrect(px, ui_rect(x, cy - 3, 14, 6), 3, th->primary); + x += 18; + } else { + ui_circle(px, x + 3, cy, 3, th->indicator); + x += 10; + } + } +} diff --git a/apps/cellphone/ui.c b/apps/cellphone/ui.c new file mode 100644 index 00000000..0c181e8d --- /dev/null +++ b/apps/cellphone/ui.c @@ -0,0 +1,545 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone UI kit: views, gestures, timers and tweens, scrolling. + */ + +#include +#include + +#include "ui-internal.h" + +#define DRAG_SLOP 6 + +/* + * Views + */ +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, + twin_window_style_t style, + 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); + if (!w) { + free(v); + return NULL; + } + v->window = w; + v->title = title; + v->paint = paint; + v->event = event; + v->destroy = destroy; + v->app = -1; + v->next_view = g_views; + g_views = v; + twin_window_set_client_data(w, v); + twin_window_set_callbacks(w, _ui_draw, _ui_event, _ui_destroy); + twin_window_set_name(w, title); + twin_window_set_opaque(w, true); + return v; +} + +void *ui_view_new(size_t size, + const char *title, + ui_paint_t paint, + ui_handler_t event, + ui_destroy_t destroy) +{ + return _ui_view_new(size, TwinWindowApplication, title, paint, event, + destroy); +} + +void *ui_view_new_fullscreen(size_t size, + const char *title, + ui_paint_t paint, + ui_handler_t event, + ui_destroy_t destroy) +{ + return _ui_view_new(size, TwinWindowFullscreen, title, paint, event, + destroy); +} + +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(); +} + +void ui_view_close(ui_view_t *view) +{ + twin_window_close(view->window); +} + +ui_view_t *ui_view_of(twin_window_t *window) +{ + for (ui_view_t *v = g_views; v && window; v = v->next_view) { + if (v->window == window) + return v; + } + return NULL; +} + +void ui_view_set_layout(ui_view_t *view, ui_layout_t layout) +{ + view->layout = layout; + if (layout) + layout(view); +} + +int ui_key_ascii(const ui_event_t *event) +{ + return event->ucs4 < 0x80 ? (int) event->ucs4 : -1; +} + +ui_rect_t ui_view_bounds(const ui_view_t *view) +{ + return ui_rect(0, 0, ui_view_width(view), ui_view_height(view)); +} + +twin_coord_t ui_view_width(const ui_view_t *view) +{ + return ui_w(twin_window_client(view->window)); +} + +twin_coord_t ui_view_height(const ui_view_t *view) +{ + return ui_h(twin_window_client(view->window)); +} + +ui_rect_t ui_view_clip(ui_view_t *view) +{ + return twin_pixmap_get_clip(twin_window_pixmap(view->window)); +} + +void ui_view_invalidate(ui_view_t *view, ui_rect_t r) +{ + twin_window_t *w = view->window; + twin_rect_t c = twin_window_client(w); + twin_window_damage(w, r.left + c.left, r.top + c.top, r.right + c.left, + r.bottom + c.top); +} + +void ui_view_invalidate_all(ui_view_t *view) +{ + ui_view_invalidate(view, ui_view_bounds(view)); +} + +void ui_view_set_pressed(ui_view_t *view, + int *slot, + int next, + ui_rect_t (*rect)(ui_view_t *view, int i)) +{ + if (*slot == next) + return; + ui_rect_t r; + if (*slot >= 0 && !ui_empty(r = rect(view, *slot))) + ui_view_invalidate(view, r); + *slot = next; + if (next >= 0 && !ui_empty(r = rect(view, next))) + ui_view_invalidate(view, r); +} + +void ui_view_scroll(ui_view_t *view, ui_rect_t r, int dx, int dy) +{ + twin_window_scroll(view->window, r, (twin_coord_t) dx, (twin_coord_t) dy); +} + +static void _ui_draw(twin_window_t *window) +{ + ui_view_t *v = twin_window_client_data(window); + if (v->paint) + v->paint(v, twin_window_pixmap(window)); +} + +/* Deliver to the handler; never touch @v afterwards (it may be gone) */ +static bool _deliver(ui_view_t *v, ui_event_t *e) +{ + return v->event ? v->event(v, e) : false; +} + +static bool _ui_event(twin_window_t *window, twin_event_t *ev) +{ + ui_view_t *v = twin_window_client_data(window); + ui_event_t e = {0}; + twin_time_t now = twin_now(); + + switch (ev->kind) { + case TwinEventButtonDown: + v->pressed = true; + v->dragging = false; + v->px0 = v->plx = ev->u.pointer.x; + v->py0 = v->ply = ev->u.pointer.y; + v->plt = now; + v->pvx = v->pvy = 0; + e.kind = UI_PRESS; + break; + case TwinEventMotion: { + if (!v->pressed) + return false; + twin_coord_t x = ev->u.pointer.x, y = ev->u.pointer.y; + twin_time_t dt = now - v->plt; + if (dt > 0) { + int vx = (x - v->plx) * 1000 / dt, vy = (y - v->ply) * 1000 / dt; + v->pvx = (v->pvx + vx) / 2; + v->pvy = (v->pvy + vy) / 2; + v->plx = x; + v->ply = y; + v->plt = now; + } + if (!v->dragging && + (abs(x - v->px0) > DRAG_SLOP || abs(y - v->py0) > DRAG_SLOP)) + v->dragging = true; + e.kind = v->dragging ? UI_DRAG : UI_MOVE; + break; + } + case TwinEventButtonUp: + if (!v->pressed) + return false; + v->pressed = false; + /* A pause before lifting the finger kills the fling */ + if (now - v->plt > 80) + v->pvx = v->pvy = 0; + e.kind = v->dragging ? UI_RELEASE : UI_TAP; + v->dragging = false; + break; + case TwinEventUcs4: + e.kind = UI_KEY; + e.ucs4 = ev->u.ucs4.ucs4; + /* Hardware Return and Delete arrive as the soft keyboard's codes */ + if (e.ucs4 == '\n') + e.ucs4 = UI_KEY_ENTER; + else if (e.ucs4 == 127) + e.ucs4 = UI_KEY_BACKSPACE; + return _deliver(v, &e); + case TwinEventConfigure: + /* Sent only when the client size changed (the work area moved) */ + ui_view_invalidate_all(v); + if (v->layout) + v->layout(v); + e.kind = UI_RESIZE; + _deliver(v, &e); + return true; + case TwinEventActivate: + case TwinEventDeactivate: + e.kind = ev->kind == TwinEventActivate ? UI_SHOW : UI_HIDE; + _deliver(v, &e); + /* A view revealed by a closing demo window: the stack changed */ + if (ev->kind == TwinEventActivate) + phone_view_changed(); + return true; + case TwinEventClose: + return false; /* let the window close */ + default: + return false; + } + e.x = ev->u.pointer.x; + e.y = ev->u.pointer.y; + e.x0 = v->px0; + e.y0 = v->py0; + e.dx = e.x - v->px0; + e.dy = e.y - v->py0; + e.vx = v->pvx; + e.vy = v->pvy; + _deliver(v, &e); + /* Pointer events are always consumed by views */ + return true; +} + +static ui_scroll_t *fling_scroll; +static ui_view_t *fling_view; + +static void _ui_destroy(twin_window_t *window) +{ + ui_view_t *v = twin_window_client_data(window); + for (ui_view_t **p = &g_views; *p; p = &(*p)->next_view) { + if (*p == v) { + *p = v->next_view; + break; + } + } + if (v->alive) + *v->alive = false; + ui_view_stop_timers(v); + ui_view_stop_tweens(v); + if (fling_view == v) { + fling_view = NULL; + fling_scroll = NULL; + } + if (v->destroy) + v->destroy(v); + free(v); + phone_view_changed(); +} + +/* + * Timers and tweens. The closure is the view's slot, so nothing extra is + * allocated. A callback may destroy its view: _guard_end() then reports + * false and the slot (inside the freed view) must not be touched. + */ +static bool *_guard_begin(ui_view_t *v, bool *alive) +{ + bool *outer = v->alive; + *alive = true; + v->alive = alive; + return outer; +} + +static bool _guard_end(ui_view_t *v, bool alive, bool *outer) +{ + if (!alive) { + if (outer) + *outer = false; + return false; + } + v->alive = outer; + return true; +} + +static twin_time_t _timer(twin_time_t now, void *closure) +{ + ui_slot_t *slot = closure; + ui_view_t *v = slot->view; + bool alive, *outer = _guard_begin(v, &alive); + (void) now; + /* The callback may stop its timers and start a new one, which can land + * in this same slot: only clear the slot if it still holds this one */ + void *self = slot->handle; + twin_time_t next = slot->fn.timer(v); + if (!_guard_end(v, alive, outer)) + return -1; /* view destroyed: the timeout is already cleared */ + if (slot->handle != self) + return -1; /* stopped from the callback: already cleared */ + if (next < 0) + slot->handle = NULL; + return next; +} + +static bool _tween(twin_fixed_t value, void *closure) +{ + ui_slot_t *slot = closure; + ui_view_t *v = slot->view; + bool alive, *outer = _guard_begin(v, &alive); + void *self = slot->handle; + bool keep = slot->fn.tween(v, value); + if (!_guard_end(v, alive, outer)) + return false; + if (slot->handle != self) + return false; /* cancelled from the callback, slot maybe reused */ + /* Only the final frame is exactly one: overshooting easings (OutBack) + * pass above it on the way and must still reach the end */ + if (!keep || value == TWIN_FIXED_ONE) { + slot->handle = NULL; + return false; + } + return true; +} + +static ui_slot_t *_free_slot(ui_view_t *view, ui_slot_t *slots) +{ + for (int i = 0; i < UI_VIEW_TIMERS; i++) { + if (!slots[i].handle) { + slots[i].view = view; + return &slots[i]; + } + } + return NULL; +} + +bool ui_view_every(ui_view_t *view, twin_time_t delay, ui_timer_t fn) +{ + ui_slot_t *slot = _free_slot(view, view->timers); + if (!slot) + return false; + slot->fn.timer = fn; + slot->handle = twin_set_timeout(_timer, delay, slot); + return slot->handle != NULL; +} + +bool ui_view_has_timer(ui_view_t *view, ui_timer_t fn) +{ + for (int i = 0; i < UI_VIEW_TIMERS; i++) + if (view->timers[i].handle && view->timers[i].fn.timer == fn) + return true; + return false; +} + +void ui_view_stop_timers(ui_view_t *view) +{ + for (int i = 0; i < UI_VIEW_TIMERS; i++) { + if (view->timers[i].handle) + twin_clear_timeout(view->timers[i].handle); + view->timers[i].handle = NULL; + } +} + +void ui_view_stop_tweens(ui_view_t *view) +{ + for (int i = 0; i < UI_VIEW_TIMERS; i++) { + if (view->tweens[i].handle) + twin_tween_cancel(view->tweens[i].handle); + view->tweens[i].handle = NULL; + } +} + +bool ui_view_tween(ui_view_t *view, + twin_time_t duration, + twin_ease_t ease, + ui_tween_t fn) +{ + ui_slot_t *slot = _free_slot(view, view->tweens); + if (!slot) + return false; + slot->fn.tween = fn; + slot->handle = twin_tween_start(duration, ease, _tween, slot); + return slot->handle != NULL; +} + +/* + * Scrolling + */ +void ui_scroll_setup(ui_scroll_t *s, ui_rect_t area, twin_coord_t content_h) +{ + s->area = area; + s->content_h = content_h; + twin_coord_t m = ui_scroll_max(s); + if (s->offset > m) + s->offset = m; + if (s->offset < 0) + s->offset = 0; +} + +twin_coord_t ui_scroll_max(const ui_scroll_t *s) +{ + twin_coord_t m = s->content_h - ui_h(s->area); + return m > 0 ? m : 0; +} + +void ui_scroll_to(ui_view_t *view, ui_scroll_t *s, twin_coord_t offset) +{ + twin_coord_t m = ui_scroll_max(s); + if (offset > m) + offset = m; + if (offset < 0) + offset = 0; + twin_coord_t d = offset - s->offset; + if (!d) + return; + s->offset = offset; + ui_view_scroll(view, s->area, 0, -d); + /* The indicator moved with the pixels: repaint its column */ + if (s->content_h > ui_h(s->area)) + ui_view_invalidate( + view, ui_rect(s->area.right - 6, s->area.top, 6, ui_h(s->area))); +} + +static twin_time_t _fling_step(ui_view_t *view) +{ + ui_scroll_t *s = fling_scroll; + if (!s || fling_view != view || !s->flinging) + return -1; + twin_time_t now = twin_now(); + twin_time_t dt = now - s->last; + s->last = now; + twin_coord_t before = s->offset; + ui_scroll_to(view, s, s->offset + s->velocity * dt / 1000); + /* Exponential friction, ~5% per frame */ + s->velocity = s->velocity * 243 / 256; + if (abs(s->velocity) < 40 || s->offset == before) { + s->flinging = false; + fling_scroll = NULL; + fling_view = NULL; + return -1; + } + return 16; +} + +bool ui_scroll_event(ui_view_t *view, ui_scroll_t *s, const ui_event_t *e) +{ + switch (e->kind) { + case UI_PRESS: + s->grab = s->offset; + s->caught = false; + if (s->flinging) { + s->flinging = false; + /* A press over the list that stops a fling is not a tap: swallow + * it and the tap it may end in */ + s->caught = ui_hit(s->area, e->x, e->y); + return s->caught; + } + return false; + case UI_TAP: + if (!s->caught) + return false; + s->caught = false; + return true; + case UI_DRAG: + if (!ui_hit(s->area, e->x0, e->y0) || abs(e->dy) < abs(e->dx)) + return false; + ui_scroll_to(view, s, s->grab - e->dy); + return true; + case UI_RELEASE: + if (!ui_hit(s->area, e->x0, e->y0) || abs(e->dy) < abs(e->dx)) + return false; + if (abs(e->vy) > 200 && ui_scroll_max(s) > 0) { + s->velocity = -e->vy; + s->last = twin_now(); + s->flinging = true; + if (fling_scroll && fling_scroll != s) + fling_scroll->flinging = false; + fling_scroll = s; + fling_view = view; + if (!ui_view_every(view, 16, _fling_step)) { + s->flinging = false; + fling_scroll = NULL; + fling_view = NULL; + } + } + return true; + default: + return false; + } +} + +void ui_scroll_bar(twin_pixmap_t *px, const ui_scroll_t *s) +{ + twin_coord_t h = ui_h(s->area); + twin_coord_t m = ui_scroll_max(s); + if (m <= 0) + return; + twin_coord_t bar = h * h / s->content_h; + if (bar < 16) + bar = 16; + twin_coord_t y = s->area.top + (h - bar) * s->offset / m; + ui_rrect(px, ui_rect(s->area.right - 4, y, 3, bar), 1, + ui_alpha(phone_theme()->text_sec, 0x90)); +} diff --git a/apps/clock.c b/apps/clock.c index 518f4084..17b2fc16 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -11,7 +11,7 @@ #include -#include "apps_clock.h" +#include "apps.h" #define D(x) twin_double_to_fixed(x) @@ -203,20 +203,22 @@ static twin_time_t _apps_clock_timeout(twin_time_t now, void *closure) return _apps_clock_interval(); } -static twin_dispatch_result_t _apps_clock_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t _apps_clock_dispatch(twin_custom_widget_t *custom, + twin_event_t *event) { - (void) closure; /* unused parameter */ - - twin_custom_widget_t *custom = twin_widget_get_custom(widget); - if (!custom) - return TwinDispatchContinue; - switch (event->kind) { case TwinEventPaint: _apps_clock_paint(custom); break; + case TwinEventDestroy: { + /* The ticking timeout would outlive the clock */ + apps_clock_data_t *data = + (apps_clock_data_t *) twin_custom_widget_data(custom); + if (data->timeout) + twin_clear_timeout(data->timeout); + data->timeout = NULL; + break; + } default: break; } @@ -226,7 +228,7 @@ static twin_dispatch_result_t _apps_clock_dispatch(twin_widget_t *widget, static twin_custom_widget_t *_apps_clock_init(twin_box_t *parent) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0, 0, 0, 1, 1, _apps_clock_dispatch, sizeof(apps_clock_data_t)); + parent, 0, 0, 0, _apps_clock_dispatch, sizeof(apps_clock_data_t)); if (!custom) return NULL; @@ -243,16 +245,20 @@ static twin_custom_widget_t *apps_clock_create(twin_box_t *parent) return _apps_clock_init(parent); } -void apps_clock_start(twin_screen_t *screen, +bool apps_clock_start(twin_screen_t *screen, const char *name, int x, int y, int w, int h) { - twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); - twin_custom_widget_t *clock = apps_clock_create(&toplevel->box); - (void) clock; + twin_toplevel_t *toplevel = twin_toplevel_create(screen, x, y, w, h, name); + if (!toplevel) + return false; + if (!apps_clock_create(twin_toplevel_box(toplevel))) { + twin_toplevel_destroy(toplevel); + return false; + } twin_toplevel_show(toplevel); + return true; } diff --git a/apps/image.c b/apps/image.c index 262fd8f5..4693535c 100644 --- a/apps/image.c +++ b/apps/image.c @@ -8,7 +8,7 @@ #include -#include "apps_image.h" +#include "apps.h" static inline twin_pixmap_t *_apps_image_pixmap(twin_custom_widget_t *image) { @@ -16,18 +16,12 @@ static inline twin_pixmap_t *_apps_image_pixmap(twin_custom_widget_t *image) } #define D(x) twin_double_to_fixed(x) -#define ASSET_PATH "assets/" #define APP_WIDTH 400 #define APP_HEIGHT 400 static twin_pixmap_t *load_tvg(const char *path) { -#if defined(CONFIG_TVG_MEMORY_BUDGET) && CONFIG_TVG_MEMORY_BUDGET > 0 - return twin_tvg_to_pixmap_budget(path, TWIN_ARGB32, - (size_t) CONFIG_TVG_MEMORY_BUDGET * 1024); -#else - return twin_tvg_to_pixmap_scale(path, TWIN_ARGB32, APP_WIDTH, APP_HEIGHT); -#endif + return twin_tvg_to_pixmap(path, APP_WIDTH, APP_HEIGHT); } typedef struct { @@ -62,20 +56,13 @@ static void _apps_image_paint(twin_custom_widget_t *custom) }; twin_composite(_apps_image_pixmap(custom), 0, 0, &srcop, 0, 0, NULL, 0, 0, - TWIN_SOURCE, img->current_pix->width, - img->current_pix->height); + TWIN_OVER, twin_pixmap_width(img->current_pix), + twin_pixmap_height(img->current_pix)); } -static twin_dispatch_result_t _apps_image_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t _apps_image_dispatch(twin_custom_widget_t *custom, + twin_event_t *event) { - (void) closure; /* unused parameter */ - - twin_custom_widget_t *custom = twin_widget_get_custom(widget); - if (!custom) - return TwinDispatchContinue; - switch (event->kind) { case TwinEventPaint: _apps_image_paint(custom); @@ -122,11 +109,11 @@ static twin_dispatch_result_t _apps_image_button_clicked(twin_widget_t *widget, return TwinDispatchDone; } -static twin_custom_widget_t *_apps_image_init(twin_box_t *parent) +static twin_custom_widget_t *_apps_image_init(twin_box_t *parent, + twin_coord_t height) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0, 0, parent->widget.window->screen->height * 3 / 4, 1, 1, - _apps_image_dispatch, sizeof(apps_image_data_t)); + parent, 0, 0, height, _apps_image_dispatch, sizeof(apps_image_data_t)); if (!custom) return NULL; @@ -138,25 +125,40 @@ static twin_custom_widget_t *_apps_image_init(twin_box_t *parent) twin_button_t *button = twin_button_create(parent, "Next Image", 0xFF482722, D(10), TwinStyleBold | TwinStyleOblique); - twin_widget_set(&button->label.widget, 0xFFFEE4CE); - twin_widget_set_callback(&button->label.widget, _apps_image_button_clicked, - custom); - button->label.widget.shape = TwinShapeRectangle; + if (!button) + return NULL; /* the toplevel's teardown frees the image widget */ + twin_widget_t *bw = twin_button_widget(button); + twin_widget_set(bw, 0xFFFEE4CE); + twin_widget_set_callback(bw, _apps_image_button_clicked, custom); + twin_widget_set_shape(bw, TwinShapeRectangle); return custom; } -static twin_custom_widget_t *apps_image_create(twin_box_t *parent) +static twin_custom_widget_t *apps_image_create(twin_box_t *parent, + twin_coord_t height) { - return _apps_image_init(parent); + return _apps_image_init(parent, height); } -void apps_image_start(twin_screen_t *screen, const char *name, int x, int y) +/* Fixed size: @w and @h are ignored */ +bool apps_image_start(twin_screen_t *screen, + const char *name, + int x, + int y, + int w, + int h) { + (void) w, (void) h; twin_toplevel_t *toplevel = - twin_toplevel_create(screen, TWIN_ARGB32, TwinWindowApplication, x, y, - APP_WIDTH, APP_HEIGHT, name); - twin_custom_widget_t *img = apps_image_create(&toplevel->box); - (void) img; + 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)) { + twin_toplevel_destroy(toplevel); + return false; + } twin_toplevel_show(toplevel); + return true; } diff --git a/apps/main.c b/apps/main.c index 29175988..50f02495 100644 --- a/apps/main.c +++ b/apps/main.c @@ -13,19 +13,12 @@ #include #include -#include "apps_animation.h" -#include "apps_calc.h" -#include "apps_clock.h" -#include "apps_image.h" -#include "apps_multi.h" -#include "apps_spline.h" +#include "apps.h" #define WIDTH 640 #define HEIGHT 480 -#define ASSET_PATH "assets/" - -#if defined(CONFIG_WINDOW_MANAGER) +#if defined(CONFIG_WM_DESKTOP) /* * Load the background pixmap from storage. * The screen compositor tiles backgrounds smaller than the screen, @@ -37,42 +30,49 @@ */ static twin_pixmap_t *load_background(twin_screen_t *screen, const char *path) { - twin_pixmap_t *raw_background = twin_pixmap_from_file(path, TWIN_ARGB32); + twin_pixmap_t *raw_background = twin_pixmap_from_file(path); if (!raw_background) return twin_make_pattern(); /* Source fits on screen -- use directly (tiles if smaller) */ - if (raw_background->width <= screen->width && - raw_background->height <= screen->height) + twin_coord_t sw = twin_screen_width(screen), + sh = twin_screen_height(screen); + twin_coord_t bw = twin_pixmap_width(raw_background); + twin_coord_t bh = twin_pixmap_height(raw_background); + if (bw <= sw && bh <= sh) return raw_background; /* Source is larger -- scale down to screen size */ - twin_pixmap_t *scaled = - twin_pixmap_create(TWIN_ARGB32, screen->width, screen->height); + twin_pixmap_t *scaled = twin_pixmap_create(TWIN_ARGB32, sw, sh); if (!scaled) { twin_pixmap_destroy(raw_background); return twin_make_pattern(); } - twin_fixed_t sx = twin_fixed_div(twin_int_to_fixed(raw_background->width), - twin_int_to_fixed(screen->width)); - twin_fixed_t sy = twin_fixed_div(twin_int_to_fixed(raw_background->height), - twin_int_to_fixed(screen->height)); - twin_matrix_scale(&raw_background->transform, sx, sy); + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_scale( + &m, twin_fixed_div(twin_int_to_fixed(bw), twin_int_to_fixed(sw)), + twin_fixed_div(twin_int_to_fixed(bh), twin_int_to_fixed(sh))); + twin_pixmap_set_transform(raw_background, &m); twin_operand_t srcop = { .source_kind = TWIN_PIXMAP, .u.pixmap = raw_background, }; - twin_composite(scaled, 0, 0, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, - screen->width, screen->height); + twin_composite(scaled, 0, 0, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, sw, sh); twin_pixmap_destroy(raw_background); return scaled; } -#endif /* CONFIG_WINDOW_MANAGER */ +#endif /* CONFIG_WM_DESKTOP */ static twin_context_t *tx = NULL; +static const apps_entry_t *shell; /* the running shell, if any */ + static void cleanup(void) { + if (shell && shell->stop) + shell->stop(); + shell = NULL; twin_destroy(tx); tx = NULL; } @@ -84,55 +84,61 @@ static void sigint_handler(int sig) exit(1); } -/* Initialize demo applications based on build configuration. +/* A shell that owns the screen (the phone, in a mobile build) */ +static const apps_entry_t *find_shell(void) +{ + for (int i = 0; i < apps_count(); i++) + if (apps_at(i)->boot) + return apps_at(i); + return NULL; +} + +/* Start the shell, or else every app in registry order. Without a window + * manager every window covers the screen, so overlapping apps would hide + * each other: start only the first one. * Shared by both native and WebAssembly builds. */ -static void init_demo_apps(twin_context_t *ctx) +static void init_demo_apps(twin_screen_t *screen) { - twin_screen_t *screen = ctx->screen; -#if defined(CONFIG_WINDOW_MANAGER) -#if defined(CONFIG_DEMO_MULTI) - apps_multi_start(screen, "Demo", 100, 100, 400, 400); -#endif -#if defined(CONFIG_DEMO_CLOCK) - apps_clock_start(screen, "Clock", 10, 10, 200, 200); -#endif -#if defined(CONFIG_DEMO_CALCULATOR) - apps_calc_start(screen, "Calculator", 100, 100, 200, 200); -#endif -#if defined(CONFIG_DEMO_SPLINE) - apps_spline_start(screen, "Spline", 20, 20, 400, 400); -#endif -#if defined(CONFIG_DEMO_ANIMATION) - apps_animation_start(screen, "Viewer", ASSET_PATH "nyancat.gif", 20, 20); -#endif -#if defined(CONFIG_DEMO_IMAGE) - apps_image_start(screen, "Viewer", 20, 20); + const apps_entry_t *sh = find_shell(); + if (sh) { + if (sh->boot(screen)) { + shell = sh; + return; + } + fprintf(stderr, "%s: out of memory, starting the apps instead\n", + sh->name); + } +#if defined(CONFIG_WM_DESKTOP) + twin_screen_set_background(screen, + load_background(screen, ASSET_PATH "tux.png")); #endif + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *app = apps_at(i); + if (app->boot) + continue; + bool started = + app->start(screen, app->name, app->x, app->y, app->w, app->h); +#if defined(CONFIG_WM_NONE) + if (started) + break; #else - /* - * Without the window manager every window is pinned at (0,0), so launching - * multiple demos leaves later windows permanently covering earlier ones. - * Start a single demo in a deterministic order instead. - */ -#if defined(CONFIG_DEMO_CLOCK) - apps_clock_start(screen, "Clock", 10, 10, 200, 200); -#elif defined(CONFIG_DEMO_CALCULATOR) - apps_calc_start(screen, "Calculator", 100, 100, 200, 200); -#elif defined(CONFIG_DEMO_SPLINE) - apps_spline_start(screen, "Spline", 20, 20, 400, 400); -#elif defined(CONFIG_DEMO_ANIMATION) - apps_animation_start(screen, "Viewer", ASSET_PATH "nyancat.gif", 20, 20); -#elif defined(CONFIG_DEMO_IMAGE) - apps_image_start(screen, "Viewer", 20, 20); -#endif + (void) started; #endif - twin_screen_set_active(screen, screen->top); + } } int main(void) { - tx = twin_create(WIDTH, HEIGHT); + int width = WIDTH, height = HEIGHT; + + const apps_entry_t *sh = find_shell(); + if (sh) { + width = sh->w; + height = sh->h; + } + + tx = twin_create(width, height); if (!tx) return 1; @@ -144,18 +150,6 @@ int main(void) atexit(cleanup); signal(SIGINT, sigint_handler); -#if defined(CONFIG_CURSOR) - int hx, hy; - twin_pixmap_t *cursor = twin_make_cursor(&hx, &hy); - if (cursor) - twin_screen_set_cursor(tx->screen, cursor, hx, hy); -#endif - -#if defined(CONFIG_WINDOW_MANAGER) - twin_screen_set_background( - tx->screen, load_background(tx->screen, ASSET_PATH "/tux.png")); -#endif - /* Start application with unified API (handles native and WebAssembly) */ twin_run(tx, init_demo_apps); diff --git a/apps/multi.c b/apps/multi.c index 5bebb93c..4aed1fc4 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -6,10 +6,10 @@ */ #include -#include "apps_multi.h" +#include +#include "apps.h" #define D(x) twin_double_to_fixed(x) -#define ASSET_PATH "assets/" typedef struct { twin_point_t p1, p2; @@ -86,16 +86,9 @@ static int _multiline_hit_test(multiline_data_t *data, return -1; } -static twin_dispatch_result_t _multiline_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t _multiline_dispatch(twin_custom_widget_t *custom, + twin_event_t *event) { - (void) closure; - - twin_custom_widget_t *custom = twin_widget_get_custom(widget); - if (!custom) - return TwinDispatchContinue; - multiline_data_t *data = (multiline_data_t *) twin_custom_widget_data(custom); @@ -106,7 +99,7 @@ static twin_dispatch_result_t _multiline_dispatch(twin_widget_t *widget, case TwinEventButtonDown: { twin_fixed_t x = twin_int_to_fixed(event->u.pointer.x); twin_fixed_t y = twin_int_to_fixed(event->u.pointer.y); - int point_idx; + int point_idx = 0; data->active_line = _multiline_hit_test(data, x, y, &point_idx); data->active_point = point_idx; twin_custom_widget_queue_paint(custom); @@ -137,95 +130,138 @@ static twin_dispatch_result_t _multiline_dispatch(twin_widget_t *widget, return TwinDispatchContinue; } -static void apps_multiline_start(twin_screen_t *screen, +static bool apps_multiline_start(twin_screen_t *screen, int x, int y, int w, int h) { - twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, "Lines"); + twin_toplevel_t *toplevel = + twin_toplevel_create(screen, x, y, w, h, "Lines"); + if (!toplevel) + return false; twin_custom_widget_t *custom = twin_custom_widget_create( - &toplevel->box, 0xffffffff, 0, 0, 1, 1, _multiline_dispatch, + twin_toplevel_box(toplevel), 0xffffffff, 0, 0, _multiline_dispatch, sizeof(multiline_data_t)); + if (!custom) { + twin_toplevel_destroy(toplevel); + return false; + } - if (custom) { - multiline_data_t *data = - (multiline_data_t *) twin_custom_widget_data(custom); - data->num_lines = 3; - data->active_line = -1; - data->line_width = D(30); - - /* Initialize 3 lines */ - twin_fixed_t fy; - int i = 0; - for (fy = 0; i < 3; fy += 50, i++) { - data->lines[i].p1.x = D(30); - data->lines[i].p1.y = D(100 - fy); - data->lines[i].p2.x = D(220); - data->lines[i].p2.y = D(100 + fy); - } + multiline_data_t *data = + (multiline_data_t *) twin_custom_widget_data(custom); + data->num_lines = 3; + data->active_line = -1; + data->line_width = D(30); + + /* Initialize 3 lines */ + twin_fixed_t fy; + int i = 0; + for (fy = 0; i < 3; fy += 50, i++) { + data->lines[i].p1.x = D(30); + data->lines[i].p1.y = D(100 - fy); + data->lines[i].p2.x = D(220); + data->lines[i].p2.y = D(100 + fy); } twin_toplevel_show(toplevel); + return true; +} + +/* + * 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. + */ + +/* Client size of the window being drawn */ +static void client_size(twin_window_t *window, int *w, int *h) +{ + twin_rect_t c = twin_window_client(window); + *w = c.right - c.left; + *h = c.bottom - c.top; +} + +/* Scale for art designed for a 400x400 window, so it fits a smaller one */ +static twin_fixed_t fit_400(int w, int h) +{ + int side = w < h ? w : h; + return side >= 400 ? TWIN_FIXED_ONE + : twin_fixed_div(twin_int_to_fixed(side), D(400)); } -static void apps_circletext_start(twin_screen_t *screen, - int x, - int y, - int w, - int h) +/* Open a window painted by @draw; @data and @destroy belong to it. A new + * window is damaged all over, so the first draw paints everything. */ +static bool start_window(twin_screen_t *screen, + int x, + int y, + int w, + int h, + const char *name, + twin_draw_func_t draw, + void *data, + twin_destroy_func_t destroy, + bool opaque) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - int wid = window->client.right - window->client.left; - int hei = window->client.bottom - window->client.top; - twin_pixmap_t *pixmap = window->pixmap; - twin_path_t *path = twin_path_create(); - twin_path_t *pen = twin_path_create(); - twin_pixmap_t *alpha = twin_pixmap_create(TWIN_A8, wid, hei); - int s; - twin_operand_t source, mask; + if (!window) + return false; + /* A window that paints every pixel opaque says so: the compositor then + * skips what lies under it */ + twin_window_set_opaque(window, opaque); + 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 */ + twin_rect_t c = twin_window_client(window); + twin_window_damage(window, c.left, c.top, c.right, c.bottom); + twin_window_show(window); + return true; +} - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, wid, hei); - twin_window_set_name(window, "circletext"); +/* Fill the client white first: these windows are declared opaque, so + * every pixel is covered even when the drawing cannot allocate a path */ +static twin_pixmap_t *clear_white(twin_window_t *window, int w, int h) +{ + twin_pixmap_t *pixmap = twin_window_pixmap(window); + twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); + return pixmap; +} +static void circletext_draw(twin_window_t *window) +{ + int w, h; + client_size(window, &w, &h); + twin_pixmap_t *pixmap = clear_white(window, w, h); + twin_path_t *path = twin_path_create(); + if (!path) + return; + twin_fixed_t scale = fit_400(w, h); twin_path_set_font_style(path, TwinStyleUnhinted); - twin_path_circle(pen, 0, 0, D(1)); - - twin_path_translate(path, D(200), D(200)); + twin_path_translate(path, twin_int_to_fixed(w / 2), + twin_int_to_fixed(h / 2)); + twin_path_scale(path, scale, scale); twin_path_set_font_size(path, D(15)); - for (s = 0; s < 41; s++) { + for (int s = 0; s < 41; s++) { twin_state_t state = twin_path_save(path); twin_path_rotate(path, twin_degrees_to_angle(9 * s)); twin_path_move(path, D(100), D(0)); twin_path_utf8(path, "Hello, world!"); twin_path_restore(path, &state); } - twin_paint_path(alpha, 0xff000000, path); + twin_paint_path(pixmap, 0xff000000, path); twin_path_destroy(path); - twin_path_destroy(pen); - source.source_kind = TWIN_SOLID; - source.u.argb = 0xff000000; - mask.source_kind = TWIN_PIXMAP; - mask.u.pixmap = alpha; - twin_composite(pixmap, 0, 0, &source, 0, 0, &mask, 0, 0, TWIN_OVER, wid, - hei); - twin_pixmap_destroy(alpha); - twin_window_show(window); } -static void apps_smiley_start(twin_screen_t *screen, int x, int y, int w, int h) +static void smiley_draw(twin_window_t *window) { - twin_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_pixmap_t *pixmap = window->pixmap; - - twin_window_set_name(window, "Smiley"); - - /* Clear background to white */ - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); + int w, h; + client_size(window, &w, &h); + twin_pixmap_t *pixmap = clear_white(window, w, h); /* Calculate center and dimensions */ twin_fixed_t center_x = twin_int_to_fixed(w / 2); @@ -263,32 +299,18 @@ static void apps_smiley_start(twin_screen_t *screen, int x, int y, int w, int h) TWIN_ANGLE_0, TWIN_ANGLE_180); twin_paint_stroke(pixmap, 0xff000000, smile, D(5)); /* Black line */ twin_path_destroy(smile); - - twin_window_show(window); } -static void apps_ascii_start(twin_screen_t *screen, int x, int y, int w, int h) +static void ascii_draw(twin_window_t *window) { - twin_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - int wid = window->client.right - window->client.left; - int hei = window->client.bottom - window->client.top; - twin_pixmap_t *pixmap = window->pixmap; + int w, h; + client_size(window, &w, &h); + twin_pixmap_t *pixmap = clear_white(window, w, h); twin_path_t *path = twin_path_create(); - twin_path_t *pen = twin_path_create(); - twin_pixmap_t *alpha = twin_pixmap_create(TWIN_A8, wid, hei); - twin_operand_t source, mask; - twin_fixed_t fx, fy; - int s; - - twin_window_set_name(window, "ASCII"); - - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, wid, hei); - twin_path_circle(pen, 0, 0, D(1)); - - fx = D(3); - fy = D(8); - for (s = 6; s < 36; s += 6) { + if (!path) + return; + twin_fixed_t fx = D(3), fy = D(8); + for (int s = 6; s < 36; s += 6) { twin_path_set_font_size(path, D(s)); fy += D(s + 2); twin_path_move(path, fx, fy); @@ -301,18 +323,8 @@ static void apps_ascii_start(twin_screen_t *screen, int x, int y, int w, int h) twin_path_utf8(path, "`abcdefghijklmnopqrstuvwxyz{|}~"); fy += D(s + 2); } - - twin_paint_path(alpha, 0xff000000, path); + twin_paint_path(pixmap, 0xff000000, path); twin_path_destroy(path); - twin_path_destroy(pen); - source.source_kind = TWIN_SOLID; - source.u.argb = 0xff000000; - mask.source_kind = TWIN_PIXMAP; - mask.u.pixmap = alpha; - twin_composite(pixmap, 0, 0, &source, 0, 0, &mask, 0, 0, TWIN_OVER, wid, - hei); - twin_pixmap_destroy(alpha); - twin_window_show(window); } static void draw_flower(twin_path_t *path, @@ -342,80 +354,151 @@ static void draw_flower(twin_path_t *path, twin_path_close(path); } -static void apps_flower_start(twin_screen_t *screen, int x, int y, int w, int h) +static void flower_draw(twin_window_t *window) { - twin_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_pixmap_t *pixmap = window->pixmap; + int w, h; + client_size(window, &w, &h); + twin_pixmap_t *pixmap = clear_white(window, w, h); + twin_fixed_t scale = fit_400(w, h); + twin_fixed_t cx = twin_int_to_fixed(w / 2), cy = twin_int_to_fixed(h / 2); twin_path_t *stroke = twin_path_create(); twin_path_t *path = twin_path_create(); - twin_path_translate(path, D(200), D(200)); - twin_path_scale(path, D(10), D(10)); - twin_path_translate(stroke, D(200), D(200)); - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); - twin_window_set_name(window, "Flower"); - twin_path_move(stroke, D(-200), D(0)); - twin_path_draw(stroke, D(200), D(0)); - twin_path_move(stroke, D(0), D(200)); - twin_path_draw(stroke, D(0), D(-200)); - twin_path_set_cap_style(stroke, TwinCapProjecting); - twin_paint_stroke(pixmap, 0xffcc9999, stroke, D(10)); - draw_flower(path, D(3), 5); - twin_paint_path(pixmap, 0xffe2d2d2, path); - twin_path_destroy(stroke); - twin_path_destroy(path); - twin_window_show(window); + if (stroke && path) { + twin_path_translate(path, cx, cy); + twin_path_scale(path, twin_fixed_mul(scale, D(10)), + twin_fixed_mul(scale, D(10))); + twin_path_translate(stroke, cx, cy); + twin_path_scale(stroke, scale, scale); + twin_path_move(stroke, D(-200), D(0)); + twin_path_draw(stroke, D(200), D(0)); + twin_path_move(stroke, D(0), D(200)); + twin_path_draw(stroke, D(0), D(-200)); + twin_path_set_cap_style(stroke, TwinCapProjecting); + twin_paint_stroke(pixmap, 0xffcc9999, stroke, + twin_fixed_mul(scale, D(10))); + draw_flower(path, D(3), 5); + twin_paint_path(pixmap, 0xffe2d2d2, path); + } + if (stroke) + twin_path_destroy(stroke); + if (path) + twin_path_destroy(path); +} + +/* + * Separable box blur, three passes approximating a Gaussian. Demo-only: + * the window system itself never blurs (shadows are synthesized). + */ +static void box_blur_line(twin_argb32_t *p, + int n, + int step, + int r, + twin_argb32_t *tmp) +{ + for (int i = 0; i < n; i++) + tmp[i] = p[i * step]; + /* sum / count as (sum * recip[count]) >> 16, exact for every count up to + * 16 and sum up to 255 * count (checked exhaustively); wider windows + * divide */ + uint32_t recip[17]; + bool fast = 2 * r + 1 <= 16; + for (int c = 1; fast && c <= 2 * r + 1; c++) + recip[c] = 65536 / c + 1; + /* Running sums over the window [i - r, i + r], clipped to the line: + * one pixel enters and one leaves per step */ + uint32_t sum[4] = {0, 0, 0, 0}; + int lo = 0, hi = 0; /* window is tmp[lo, hi) */ + for (int i = 0; i < n; i++) { + for (; hi < n && hi <= i + r; hi++) + for (int c = 0; c < 4; c++) + sum[c] += (tmp[hi] >> (c * 8)) & 0xff; + for (; lo < i - r; lo++) + for (int c = 0; c < 4; c++) + sum[c] -= (tmp[lo] >> (c * 8)) & 0xff; + uint32_t count = (uint32_t) (hi - lo); + twin_argb32_t v = 0; + for (int c = 0; c < 4; c++) + v |= (twin_argb32_t) (fast ? (sum[c] * recip[count]) >> 16 + : sum[c] / count) + << (c * 8); + p[i * step] = v; + } } -static void apps_blur(twin_screen_t *screen, int x, int y, int w, int h) +static void box_blur(twin_pixmap_t *px, int r) { - twin_pixmap_t *raw_background = NULL; -#if defined(CONFIG_LOADER_PNG) - raw_background = twin_pixmap_from_file(ASSET_PATH "tux.png", TWIN_ARGB32); -#endif - if (!raw_background) + int w = twin_pixmap_width(px), h = twin_pixmap_height(px); + twin_coord_t stride_bytes; + twin_argb32_t *pixels = twin_pixmap_pixels(px, &stride_bytes); + int stride = stride_bytes / 4; + if (!pixels) return; - twin_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_window_set_name(window, "Blur"); - int client_width = window->client.right - window->client.left; - int client_height = window->client.bottom - window->client.top; - twin_pixmap_t *scaled_background = - twin_pixmap_create(TWIN_ARGB32, client_width, client_height); - twin_fixed_t sx, sy; - sx = twin_fixed_div(twin_int_to_fixed(raw_background->width), - twin_int_to_fixed(client_width)); - sy = twin_fixed_div(twin_int_to_fixed(raw_background->height), - twin_int_to_fixed(client_height)); - - twin_matrix_scale(&raw_background->transform, sx, sy); + twin_argb32_t *tmp = malloc(sizeof(*tmp) * (w > h ? w : h)); + if (!tmp) + return; + for (int pass = 0; pass < 3; pass++) { + for (int y = 0; y < h; y++) + box_blur_line(pixels + y * stride, w, 1, r / 3 + 1, tmp); + for (int x = 0; x < w; x++) + box_blur_line(pixels + x, h, stride, r / 3 + 1, tmp); + } + free(tmp); +} + +/* The window keeps the source image and blurs it at the window's size */ +static void blur_draw(twin_window_t *window) +{ + twin_pixmap_t *raw_background = twin_window_client_data(window); + int w, h; + client_size(window, &w, &h); + twin_pixmap_t *scaled = twin_pixmap_create(TWIN_ARGB32, w, h); + if (!scaled) + return; + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_scale( + &m, + twin_fixed_div(twin_int_to_fixed(twin_pixmap_width(raw_background)), + twin_int_to_fixed(w)), + twin_fixed_div(twin_int_to_fixed(twin_pixmap_height(raw_background)), + twin_int_to_fixed(h))); + twin_pixmap_set_transform(raw_background, &m); twin_operand_t srcop = { .source_kind = TWIN_PIXMAP, .u.pixmap = raw_background, }; - - twin_composite(scaled_background, 0, 0, &srcop, 0, 0, 0, 0, 0, TWIN_SOURCE, - scaled_background->width, scaled_background->height); - - /* - * Apply blur effect to the scaled background. The valid kernel size range - * is from 1 to 15. - */ - twin_stack_blur(scaled_background, 15, 0, scaled_background->width - 1, 0, - scaled_background->height - 1); + twin_composite(scaled, 0, 0, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, w, h); + box_blur(scaled, 8); /* Copy the blurred background to the window */ - srcop.u.pixmap = scaled_background; - twin_composite(window->pixmap, 0, 0, &srcop, 0, 0, NULL, 0, 0, TWIN_SOURCE, - client_width, client_height); + srcop.u.pixmap = scaled; + twin_composite(twin_window_pixmap(window), 0, 0, &srcop, 0, 0, NULL, 0, 0, + TWIN_SOURCE, w, h); + twin_pixmap_destroy(scaled); +} - twin_pixmap_destroy(scaled_background); +static void blur_destroy(twin_window_t *window) +{ + twin_pixmap_destroy(twin_window_client_data(window)); +} + +static bool apps_blur(twin_screen_t *screen, int x, int y, int w, int h) +{ + twin_pixmap_t *raw_background = NULL; +#if defined(CONFIG_LOADER_PNG) + raw_background = twin_pixmap_from_file(ASSET_PATH "tux.png"); +#endif + if (!raw_background) + return false; + /* The image may be translucent */ + if (start_window(screen, x, y, w, h, "Blur", blur_draw, raw_background, + blur_destroy, false)) + return true; twin_pixmap_destroy(raw_background); - twin_window_show(window); - return; + return false; } -void apps_multi_start(twin_screen_t *screen, +bool apps_multi_start(twin_screen_t *screen, const char *name, int x, int y, @@ -423,10 +506,16 @@ void apps_multi_start(twin_screen_t *screen, int h) { (void) name; - apps_circletext_start(screen, x, y, w, h); - apps_multiline_start(screen, x += 20, y += 20, w * 2 / 3, h * 2 / 3); - apps_smiley_start(screen, x += 20, y += 20, 200, 200); - apps_ascii_start(screen, x += 20, y += 20, w, h); - apps_flower_start(screen, x += 20, y += 20, w, h); - apps_blur(screen, x += 20, y += 20, w / 2, h / 2); + /* Started if any of its windows opened */ + bool any = start_window(screen, x, y, w, h, "circletext", circletext_draw, + NULL, NULL, true); + any |= apps_multiline_start(screen, x += 20, y += 20, w * 2 / 3, h * 2 / 3); + any |= start_window(screen, x += 20, y += 20, 200, 200, "Smiley", + smiley_draw, NULL, NULL, true); + any |= start_window(screen, x += 20, y += 20, w, h, "ASCII", ascii_draw, + NULL, NULL, true); + any |= start_window(screen, x += 20, y += 20, w, h, "Flower", flower_draw, + NULL, NULL, true); + any |= apps_blur(screen, x += 20, y += 20, w / 2, h / 2); + return any; } diff --git a/apps/spline.c b/apps/spline.c index 4d339990..03bd648c 100644 --- a/apps/spline.c +++ b/apps/spline.c @@ -9,7 +9,7 @@ #include -#include "apps_spline.h" +#include "apps.h" #define D(x) twin_double_to_fixed(x) #define CONTROL_POINT_RADIUS 10 @@ -142,10 +142,9 @@ static twin_dispatch_result_t _apps_spline_update_pos( twin_fixed_t x = twin_int_to_fixed(event->u.pointer.x); twin_fixed_t y = twin_int_to_fixed(event->u.pointer.y); - spline->points[spline->which].x = - twin_matrix_transform_x(&(spline->inverse_transition), x, y); - spline->points[spline->which].y = - twin_matrix_transform_y(&(spline->inverse_transition), x, y); + twin_matrix_transform(&spline->inverse_transition, x, y, + &spline->points[spline->which].x, + &spline->points[spline->which].y); twin_custom_widget_queue_paint(custom); /* Parent will be repainted automatically */ return TwinDispatchDone; @@ -157,10 +156,9 @@ static int _apps_spline_hit(apps_spline_data_t *spline, { int i; for (i = 0; i < spline->n_points; i++) { - twin_fixed_t px = twin_matrix_transform_x( - &(spline->transition), spline->points[i].x, spline->points[i].y); - twin_fixed_t py = twin_matrix_transform_y( - &(spline->transition), spline->points[i].x, spline->points[i].y); + twin_fixed_t px, py; + twin_matrix_transform(&spline->transition, spline->points[i].x, + spline->points[i].y, &px, &py); if (twin_fixed_abs(x - px) < twin_int_to_fixed(CONTROL_POINT_RADIUS) && twin_fixed_abs(y - py) < twin_int_to_fixed(CONTROL_POINT_RADIUS)) return i; @@ -168,16 +166,10 @@ static int _apps_spline_hit(apps_spline_data_t *spline, return -1; } -static twin_dispatch_result_t _apps_spline_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t _apps_spline_dispatch( + twin_custom_widget_t *custom, + twin_event_t *event) { - (void) closure; /* unused parameter */ - - twin_custom_widget_t *custom = twin_widget_get_custom(widget); - if (!custom) - return TwinDispatchContinue; - apps_spline_data_t *spline = (apps_spline_data_t *) twin_custom_widget_data(custom); @@ -201,17 +193,23 @@ static twin_dispatch_result_t _apps_spline_dispatch(twin_widget_t *widget, spline->which = -1; return TwinDispatchDone; break; + case TwinEventDestroy: + free(spline->points); + spline->points = NULL; + break; default: break; } return TwinDispatchContinue; } -static twin_custom_widget_t *_apps_spline_init(twin_box_t *parent, int n_points) +static twin_custom_widget_t *_apps_spline_init(twin_box_t *parent, + int n_points, + twin_coord_t height) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0xffffffff, 0, parent->widget.window->screen->height * 2 / 3, 1, - 1, _apps_spline_dispatch, sizeof(apps_spline_data_t)); + parent, 0xffffffff, 0, height, _apps_spline_dispatch, + sizeof(apps_spline_data_t)); if (!custom) return NULL; @@ -231,30 +229,36 @@ static twin_custom_widget_t *_apps_spline_init(twin_box_t *parent, int n_points) twin_button_t *button = twin_button_create(parent, "Switch curve", 0xffae0000, D(10), TwinStyleBold | TwinStyleOblique); - twin_widget_set(&button->label.widget, 0xc0808080); - twin_widget_set_callback(&button->label.widget, _apps_spline_button_clicked, - custom); - button->label.widget.shape = TwinShapeRectangle; + twin_widget_t *bw = twin_button_widget(button); + twin_widget_set(bw, 0xc0808080); + twin_widget_set_callback(bw, _apps_spline_button_clicked, custom); + twin_widget_set_shape(bw, TwinShapeRectangle); return custom; } static twin_custom_widget_t *apps_spline_create(twin_box_t *parent, - int n_points) + int n_points, + twin_coord_t height) { - return _apps_spline_init(parent, n_points); + return _apps_spline_init(parent, n_points, height); } -void apps_spline_start(twin_screen_t *screen, +bool apps_spline_start(twin_screen_t *screen, const char *name, int x, int y, int w, int h) { - twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); - twin_custom_widget_t *spline = apps_spline_create(&toplevel->box, 4); - (void) spline; + twin_toplevel_t *toplevel = twin_toplevel_create(screen, x, y, w, h, name); + if (!toplevel) + return false; + if (!apps_spline_create(twin_toplevel_box(toplevel), 4, + twin_screen_height(screen) * 2 / 3)) { + twin_toplevel_destroy(toplevel); + return false; + } twin_toplevel_show(toplevel); + return true; } diff --git a/assets/photos/ATTRIBUTION.md b/assets/photos/ATTRIBUTION.md new file mode 100644 index 00000000..2a92367b --- /dev/null +++ b/assets/photos/ATTRIBUTION.md @@ -0,0 +1,32 @@ +Public-domain JPEG sources used by the `Photos` demo test fixture set: + +- `bob_kerrey_portrait.jpg` + Source: https://commons.wikimedia.org/wiki/File:Bob_Kerrey_full_portrait.jpg + Public-domain basis: U.S. federal government work + +- `landscape_1677.jpg` + Source: https://commons.wikimedia.org/wiki/File:Landscape_(1677).jpg + Public-domain basis: `PD-old` + +- `us_constitution.jpg` + Source: https://commons.wikimedia.org/wiki/File:Constitution.jpg + Public-domain basis: `PD-US` + +- `solar_magnetic_loops.jpg` + Source: https://commons.wikimedia.org/wiki/File:SolarMagneticLoopsNASA.jpg + Public-domain basis: NASA / U.S. federal government work + +- `apollo16_jump.jpg` + Source: https://commons.wikimedia.org/wiki/File:APOLLO16still.JPG + Public-domain basis: NASA / U.S. federal government work + +- `iss_window.jpg` + Source: https://commons.wikimedia.org/wiki/File:Iss068e006913.jpeg + Public-domain basis: NASA / U.S. federal government work + +Files were fetched via Wikimedia Commons `Special:FilePath` width-limited JPEG +redirects so the demo carries modest-size fixtures while still exercising the +real filesystem + JPEG decoder path. + +The `thumbs/` and `view/` JPEGs are local display-optimized derivatives made +from those source fixtures for smoother browsing in the cellphone demo. diff --git a/assets/photos/thumbs/apollo16_jump_thumb.jpg b/assets/photos/thumbs/apollo16_jump_thumb.jpg new file mode 100644 index 00000000..3b750655 Binary files /dev/null and b/assets/photos/thumbs/apollo16_jump_thumb.jpg differ diff --git a/assets/photos/thumbs/bob_kerrey_portrait_thumb.jpg b/assets/photos/thumbs/bob_kerrey_portrait_thumb.jpg new file mode 100644 index 00000000..5549704b Binary files /dev/null and b/assets/photos/thumbs/bob_kerrey_portrait_thumb.jpg differ diff --git a/assets/photos/thumbs/iss_window_thumb.jpg b/assets/photos/thumbs/iss_window_thumb.jpg new file mode 100644 index 00000000..ecfc6593 Binary files /dev/null and b/assets/photos/thumbs/iss_window_thumb.jpg differ diff --git a/assets/photos/thumbs/landscape_1677_thumb.jpg b/assets/photos/thumbs/landscape_1677_thumb.jpg new file mode 100644 index 00000000..184a05ef Binary files /dev/null and b/assets/photos/thumbs/landscape_1677_thumb.jpg differ diff --git a/assets/photos/thumbs/solar_magnetic_loops_thumb.jpg b/assets/photos/thumbs/solar_magnetic_loops_thumb.jpg new file mode 100644 index 00000000..f83fed05 Binary files /dev/null and b/assets/photos/thumbs/solar_magnetic_loops_thumb.jpg differ diff --git a/assets/photos/thumbs/us_constitution_thumb.jpg b/assets/photos/thumbs/us_constitution_thumb.jpg new file mode 100644 index 00000000..7b2f753b Binary files /dev/null and b/assets/photos/thumbs/us_constitution_thumb.jpg differ diff --git a/assets/photos/view/apollo16_jump_view.jpg b/assets/photos/view/apollo16_jump_view.jpg new file mode 100644 index 00000000..10c841a4 Binary files /dev/null and b/assets/photos/view/apollo16_jump_view.jpg differ diff --git a/assets/photos/view/bob_kerrey_portrait_view.jpg b/assets/photos/view/bob_kerrey_portrait_view.jpg new file mode 100644 index 00000000..ef4a3a78 Binary files /dev/null and b/assets/photos/view/bob_kerrey_portrait_view.jpg differ diff --git a/assets/photos/view/iss_window_view.jpg b/assets/photos/view/iss_window_view.jpg new file mode 100644 index 00000000..a8f9b3c8 Binary files /dev/null and b/assets/photos/view/iss_window_view.jpg differ diff --git a/assets/photos/view/landscape_1677_view.jpg b/assets/photos/view/landscape_1677_view.jpg new file mode 100644 index 00000000..de1e03ce Binary files /dev/null and b/assets/photos/view/landscape_1677_view.jpg differ diff --git a/assets/photos/view/solar_magnetic_loops_view.jpg b/assets/photos/view/solar_magnetic_loops_view.jpg new file mode 100644 index 00000000..5cdb4042 Binary files /dev/null and b/assets/photos/view/solar_magnetic_loops_view.jpg differ diff --git a/assets/photos/view/us_constitution_view.jpg b/assets/photos/view/us_constitution_view.jpg new file mode 100644 index 00000000..e23b93ae Binary files /dev/null and b/assets/photos/view/us_constitution_view.jpg differ diff --git a/backend/drm.c b/backend/drm.c index 7a8358c5..133e81ae 100644 --- a/backend/drm.c +++ b/backend/drm.c @@ -14,526 +14,228 @@ #include #include -#include "linux_input.h" -#include "linux_vt.h" -#include "twin_private.h" +#include "linux_fb.h" #define DRM_DRI_NAME "DRMDEVICE" #define DRM_DRI_DEFAULT "/dev/dri/card0" -#define SCREEN(x) ((twin_context_t *) x)->screen -#define RES(x) ((twin_drm_t *) x)->res -#define CONN(x) ((twin_drm_t *) x)->conn -#define CRTC(x) ((twin_drm_t *) x)->crtc -#define PRIV(x) ((twin_drm_t *) ((twin_context_t *) x)->priv) typedef struct { - twin_screen_t *screen; - - /* Linux input system */ - void *input; - - /* Linux virtual terminal (VT) */ - int vt_fd; - bool vt_active; - struct vt_mode old_vtm; - - /* DRM driver */ - int width, height; - int bpp, depth; - int fix_length; - uint32_t dumb_id; - size_t fb_len; - uint32_t fb_id; - uint8_t *fb_base; - - int drm_dri_fd; - - drmModeResPtr res; + twin_linux_fb_t fb; /* first: the shared core reads it from priv */ + int fd; drmModeConnectorPtr conn; - drmModeCrtcPtr crtc; - - /* Initialization state */ - bool first_run; + drmModeModeInfo mode; + uint32_t crtc_id; + drmModeCrtcPtr saved_crtc; /* the console's setup, restored on exit */ + int bpp, depth; + uint32_t dumb, fb_id; + uint64_t map_offset; } twin_drm_t; -static void twin_drm_get_screen_size(twin_drm_t *tx, int *width, int *height) -{ - *width = CONN(tx)->modes[0].hdisplay; - *height = CONN(tx)->modes[0].vdisplay; -} - -static bool get_resources(int fd, drmModeResPtr *resources) -{ - *resources = drmModeGetResources(fd); - if (*resources == NULL) { - log_error("Failed to get resources"); - return false; - } - return true; -} - -static drmModeConnectorPtr find_drm_connector(int fd, drmModeResPtr resources) -{ - drmModeConnectorPtr connector_ptr = NULL; - - for (int i = 0; i < resources->count_connectors; i++) { - connector_ptr = drmModeGetConnector(fd, resources->connectors[i]); - if (connector_ptr && connector_ptr->connection == DRM_MODE_CONNECTED) { - /* it's connected*/ - if (connector_ptr->count_modes > 0) { - /* there is at least one valid mode */ - break; - } - } - drmModeFreeConnector(connector_ptr); - connector_ptr = NULL; - } - - return connector_ptr; -} - -static bool create_fb(twin_drm_t *tx, int32_t crtc_id) +static drmModeConnectorPtr find_connector(int fd, drmModeResPtr res) { - /* Try to get framebuffer information */ - drmModeCrtcPtr crtc = drmModeGetCrtc(tx->drm_dri_fd, crtc_id); - if (crtc && crtc->buffer_id) { - drmModeFBPtr fb = drmModeGetFB(tx->drm_dri_fd, crtc->buffer_id); - if (fb) { - tx->bpp = fb->bpp; - tx->depth = fb->depth; - drmModeFreeFB(fb); - } - drmModeFreeCrtc(crtc); - } else { - if (crtc) - drmModeFreeCrtc(crtc); - return false; + for (int i = 0; i < res->count_connectors; i++) { + drmModeConnectorPtr c = drmModeGetConnector(fd, res->connectors[i]); + if (c && c->connection == DRM_MODE_CONNECTED && c->count_modes > 0) + return c; + drmModeFreeConnector(c); } - - /* Validate bits per pixel */ - if (tx->bpp != 16 && tx->bpp != 24 && tx->bpp != 32) { - log_error("Unsupported bits per pixel: %d", tx->bpp); - return false; - } - - /* Create dumb buffer */ - struct drm_mode_create_dumb create_req = { - .width = tx->width, - .height = tx->height, - .bpp = tx->bpp, - }; - if (drmIoctl(tx->drm_dri_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_req) < 0) { - log_error("Cannot create dumb buffer"); - return false; - } - tx->dumb_id = create_req.handle; - tx->fb_len = create_req.size; - tx->fix_length = create_req.pitch; - - /* Create framebuffer object for the dumb-buffer */ - if (drmModeAddFB(tx->drm_dri_fd, tx->width, tx->height, tx->depth, tx->bpp, - create_req.pitch, create_req.handle, &tx->fb_id) != 0) { - log_error("Cannot create framebuffer"); - goto bail_dumb; - } - - /* Prepare buffer for memory mapping */ - struct drm_mode_map_dumb map_req = { - .handle = create_req.handle, - }; - if (drmIoctl(tx->drm_dri_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_req) < 0) { - log_error("Cannot map dumb buffer"); - goto bail_fb; - } - - /* Perform actual memory mapping */ - tx->fb_base = mmap(NULL, tx->fb_len, PROT_READ | PROT_WRITE, MAP_SHARED, - tx->drm_dri_fd, map_req.offset); - if (tx->fb_base == MAP_FAILED) { - log_error("Failed to mmap framebuffer"); - goto bail_fb; - } - - return true; - -bail_fb: - drmModeRmFB(tx->drm_dri_fd, tx->fb_id); -bail_dumb: - struct drm_mode_destroy_dumb destroy = {.handle = tx->dumb_id}; - drmIoctl(tx->drm_dri_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); - return false; -} - -static void destroy_fb(twin_drm_t *tx) -{ - /* Unmap framebuffer */ - munmap(tx->fb_base, tx->fb_len); - - /* Delete framebuffer */ - drmModeRmFB(tx->drm_dri_fd, tx->fb_id); - - /* Delete dumb buffer */ - struct drm_mode_destroy_dumb destroy = {.handle = tx->dumb_id}; - drmIoctl(tx->drm_dri_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); + log_error("No connected display"); + return NULL; } -static int32_t find_drm_crtc(twin_drm_t *tx) +/* The CRTC driving the connector now, else any its encoders can use */ +static int64_t find_crtc(int fd, drmModeResPtr res, drmModeConnectorPtr conn) { - drmModeEncoderPtr enc = NULL; - int32_t crtc_id = -1; - - /* First try the currently conected encoder+crtc */ - enc = drmModeGetEncoder(tx->drm_dri_fd, CONN(tx)->encoder_id); + drmModeEncoderPtr enc = drmModeGetEncoder(fd, conn->encoder_id); if (enc) { - drmModeCrtcPtr crtc = drmModeGetCrtc(tx->drm_dri_fd, enc->crtc_id); - if (crtc) { - crtc_id = enc->crtc_id; - drmModeFreeCrtc(crtc); - drmModeFreeEncoder(enc); - return crtc_id; - } - + uint32_t id = enc->crtc_id; drmModeFreeEncoder(enc); + if (id) + return id; } - - /* Iterate all encoders,find supported CRTC */ - for (int i = 0; i < CONN(tx)->count_encoders; ++i) { - enc = drmModeGetEncoder(tx->drm_dri_fd, CONN(tx)->encoders[i]); + for (int i = 0; i < conn->count_encoders; i++) { + enc = drmModeGetEncoder(fd, conn->encoders[i]); if (!enc) continue; - /* Iterate all CRTCs */ - for (int j = 0; j < RES(tx)->count_crtcs; ++j) { - /* Check whether this CRTC works with the encoder */ - if (enc->possible_crtcs & (1 << j)) { - crtc_id = RES(tx)->crtcs[j]; - drmModeFreeEncoder(enc); - return crtc_id; - } - } + uint32_t possible = enc->possible_crtcs; drmModeFreeEncoder(enc); + for (int j = 0; j < res->count_crtcs; j++) + if (possible & (1u << j)) + return res->crtcs[j]; } - - log_error("No valid CRTC found"); - return crtc_id; + log_error("No usable CRTC"); + return -1; } -static bool twin_drm_apply_config(twin_drm_t *tx) +/* Pixel depth of what the CRTC shows now; XRGB8888 when it shows nothing */ +static void probe_depth(twin_drm_t *tx) { - /* Retrieve resources */ - if (!get_resources(tx->drm_dri_fd, &RES(tx))) - return false; - - /* Find a connected connector */ - CONN(tx) = find_drm_connector(tx->drm_dri_fd, RES(tx)); - if (!CONN(tx)) - goto bail_res; - - int32_t crtc_id = find_drm_crtc(tx); - if (crtc_id == -1) - goto bail_conn; - - /* Get the mode information */ - drmModeModeInfo mode = CONN(tx)->modes[0]; - tx->width = mode.hdisplay; - tx->height = mode.vdisplay; - - /* Create the framebuffer */ - if (!create_fb(tx, crtc_id)) - goto bail_conn; - - /* Set the mode on a CRTC */ - CRTC(tx) = drmModeGetCrtc(tx->drm_dri_fd, crtc_id); - if (!CRTC(tx)) - goto bail_mmap; - - if (drmModeSetCrtc(tx->drm_dri_fd, CRTC(tx)->crtc_id, tx->fb_id, 0, 0, - &CONN(tx)->connector_id, 1, &mode) != 0) { - log_error("Failed to set CRTC"); - goto bail_crtc; + tx->bpp = 32; + tx->depth = 24; + drmModeCrtcPtr crtc = tx->saved_crtc; + if (!crtc || !crtc->buffer_id) + return; + drmModeFBPtr fb = drmModeGetFB(tx->fd, crtc->buffer_id); + if (!fb) + return; + if (fb->bpp == 16 || fb->bpp == 24 || fb->bpp == 32) { + tx->bpp = fb->bpp; + tx->depth = fb->depth; } - - return true; - -bail_crtc: - drmModeFreeCrtc(CRTC(tx)); -bail_mmap: - destroy_fb(tx); -bail_conn: - drmModeFreeConnector(CONN(tx)); -bail_res: - drmModeFreeResources(RES(tx)); - return false; + drmModeFreeFB(fb); } -static bool twin_drm_open(twin_drm_t *tx, const char *path) +/* Map the dumb buffer and show it: at start and on every return to the + * VT, whose owner may have changed the mode meanwhile */ +static bool twin_drm_map(twin_linux_fb_t *fb) { - int fd; - uint64_t has_dumb; - - fd = open(path, O_RDWR); - if (fd < 0) { - log_error("Failed to open %s", path); + twin_drm_t *tx = (twin_drm_t *) fb; + fb->fb_base = mmap(NULL, fb->fb_len, PROT_READ | PROT_WRITE, MAP_SHARED, + tx->fd, tx->map_offset); + if (fb->fb_base == MAP_FAILED) { + log_error("Failed to mmap framebuffer"); return false; } - - if (drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &has_dumb) < 0 || !has_dumb) { - log_error("Drm device '%s' does not support dumb buffers", path); - close(fd); + if (drmModeSetCrtc(tx->fd, tx->crtc_id, tx->fb_id, 0, 0, + &tx->conn->connector_id, 1, &tx->mode)) { + log_error("Failed to set CRTC"); + twin_linux_fb_unmap(fb); return false; } - - tx->drm_dri_fd = fd; return true; } -static void twin_drm_damage(twin_drm_t *tx) -{ - int width, height; - twin_drm_get_screen_size(tx, &width, &height); - twin_screen_damage(tx->screen, 0, 0, width, height); -} - -static void twin_drm_flush(twin_drm_t *tx) +static void twin_drm_flush(twin_linux_fb_t *fb) { - drmModeDirtyFB(tx->drm_dri_fd, tx->fb_id, NULL, 0); + twin_drm_t *tx = (twin_drm_t *) fb; + drmModeDirtyFB(tx->fd, tx->fb_id, NULL, 0); } -static bool twin_drm_update_damage(void *closure) +/* Pick the display and create the buffer the screen draws into */ +static bool twin_drm_setup(twin_drm_t *tx) { - twin_drm_t *tx = PRIV(closure); - twin_screen_t *screen = SCREEN(closure); - - if (!tx->vt_active && (tx->fb_base != MAP_FAILED) && - twin_screen_damaged(screen)) { - twin_screen_update(screen); - twin_drm_flush(tx); - } - - return true; -} - -static bool twin_drm_work(void *closure) -{ - twin_drm_t *tx = PRIV(closure); - twin_screen_t *screen = SCREEN(closure); - - /* Mark entire screen as damaged on first run to ensure initial rendering. - * This is necessary because the screen content is not automatically - * rendered when the application starts. Using per-context state allows - * proper reinitialization if the backend is torn down and recreated. - */ - if (tx->first_run) { - tx->first_run = false; - twin_screen_damage(screen, 0, 0, screen->width, screen->height); + drmModeResPtr res = drmModeGetResources(tx->fd); + if (!res) { + log_error("Failed to get DRM resources"); + return false; } - - if (tx->vt_active && (tx->fb_base != MAP_FAILED)) { - /* Unmap the drm device */ - munmap(tx->fb_base, tx->fb_len); - tx->fb_base = MAP_FAILED; + tx->conn = find_connector(tx->fd, res); + int64_t crtc = tx->conn ? find_crtc(tx->fd, res, tx->conn) : -1; + drmModeFreeResources(res); + if (crtc < 0) + return false; + tx->crtc_id = (uint32_t) crtc; + tx->mode = tx->conn->modes[0]; + tx->saved_crtc = drmModeGetCrtc(tx->fd, tx->crtc_id); + probe_depth(tx); + + struct drm_mode_create_dumb create = { + .width = tx->mode.hdisplay, + .height = tx->mode.vdisplay, + .bpp = (uint32_t) tx->bpp, + }; + if (drmIoctl(tx->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) { + log_error("Cannot create dumb buffer"); + return false; } - - if (!tx->vt_active && (tx->fb_base == MAP_FAILED)) { - /* Restore the drm device settings */ - if (!twin_drm_apply_config(tx)) - log_error("Failed to apply configurations to the drm device"); - - /* Mark entire screen for refresh */ - twin_screen_damage(screen, 0, 0, screen->width, screen->height); + tx->dumb = create.handle; + tx->fb.fb_len = create.size; + tx->fb.stride = create.pitch; + if (drmModeAddFB(tx->fd, create.width, create.height, (uint8_t) tx->depth, + (uint8_t) tx->bpp, create.pitch, create.handle, + &tx->fb_id)) { + log_error("Cannot create framebuffer"); + return false; } - - if (!tx->vt_active && twin_screen_damaged(screen)) { - twin_screen_update(screen); - twin_drm_flush(tx); + struct drm_mode_map_dumb map = {.handle = create.handle}; + if (drmIoctl(tx->fd, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { + log_error("Cannot map dumb buffer"); + return false; } - - return true; + tx->map_offset = map.offset; + return twin_drm_map(&tx->fb); } -#define ARGB32_TO_RGB565_PERLINE(dest, pixels, width) \ - do { \ - uint16_t *_dest = (uint16_t *) dest; \ - for (int i = 0; i < width; i++) \ - _dest[i] = ((pixels[i] & 0x00f80000) >> 8) | \ - ((pixels[i] & 0x0000fc00) >> 5) | \ - ((pixels[i] & 0x000000f8) >> 3); \ - } while (0) - -/* Requires validation in 24-bit per pixel environments */ -#define ARGB32_TO_RGB888_PERLINE(dest, pixels, width) \ - do { \ - uint8_t *_dest = (uint8_t *) dest; \ - for (int i = 0; i < width; i++) { \ - _dest[i * 3] = pixels[i] & 0xFF; \ - _dest[i * 3 + 1] = ((pixels[i] & 0x0000FF00) >> 8); \ - _dest[i * 3 + 2] = ((pixels[i] & 0x00FF0000) >> 16); \ - } \ - } while (0) - -#define ARGB32_TO_ARGB32_PERLINE(dest, pixels, width) \ - memcpy((uint32_t *) (dest), (pixels), (width) * sizeof(uint32_t)) - -#define DRM_PUT_SPAN_IMPL(bpp, op) \ - static void _twin_drm_put_span##bpp(twin_coord_t left, twin_coord_t top, \ - twin_coord_t right, \ - twin_argb32_t *pixels, void *closure) \ - { \ - twin_drm_t *tx = PRIV(closure); \ - off_t off = (bpp / 8) * left + top * tx->fix_length; \ - uintptr_t dest = (uintptr_t) tx->fb_base + off; \ - twin_coord_t width = right - left; \ - op(dest, pixels, width); \ +/* Undo twin_drm_setup(), whatever part of it ran, and close the device */ +static void twin_drm_teardown(twin_drm_t *tx) +{ + twin_linux_fb_unmap(&tx->fb); + drmModeCrtcPtr c = tx->saved_crtc; + if (c && c->mode_valid) + drmModeSetCrtc(tx->fd, c->crtc_id, c->buffer_id, c->x, c->y, + &tx->conn->connector_id, 1, &c->mode); + if (tx->fb_id) + drmModeRmFB(tx->fd, tx->fb_id); + if (tx->dumb) { + struct drm_mode_destroy_dumb destroy = {.handle = tx->dumb}; + drmIoctl(tx->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); } - -DRM_PUT_SPAN_IMPL(16, ARGB32_TO_RGB565_PERLINE) -DRM_PUT_SPAN_IMPL(24, ARGB32_TO_RGB888_PERLINE) -DRM_PUT_SPAN_IMPL(32, ARGB32_TO_ARGB32_PERLINE) + drmModeFreeCrtc(tx->saved_crtc); + drmModeFreeConnector(tx->conn); + close(tx->fd); +} twin_context_t *twin_drm_init(int width maybe_unused, int height maybe_unused) { - /* Get environment variable to execute */ - char *drm_dri_path = getenv(DRM_DRI_NAME); - if (!drm_dri_path) { + char *path = getenv(DRM_DRI_NAME); + if (!path) { log_info("Environment variable $DRMDEVICE not set, use %s by default", DRM_DRI_DEFAULT); - drm_dri_path = DRM_DRI_DEFAULT; + path = DRM_DRI_DEFAULT; } twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); if (!ctx) return NULL; - - ctx->priv = calloc(1, sizeof(twin_drm_t)); - if (!ctx->priv) { - free(ctx); - return NULL; - } - - twin_drm_t *tx = ctx->priv; - - /* Initialize first_run flag for initial screen damage */ - tx->first_run = true; - - /* Open the DRM driver */ - if (!twin_drm_open(tx, drm_dri_path)) { + twin_drm_t *tx = ctx->priv = calloc(1, sizeof(twin_drm_t)); + if (!tx) goto bail; - } - - /* Set up virtual terminal environment */ - if (!twin_vt_setup(&tx->vt_fd, &tx->old_vtm, &tx->vt_active)) { - goto bail_dri_dev_fd; - } - - /* Set up signal handlers for switching TTYs */ - twin_vt_setup_signal_handler(); + tx->fb.fb_base = MAP_FAILED; + tx->fb.remap = twin_drm_map; + tx->fb.flush = twin_drm_flush; - /* Apply configurations to the DRM driver*/ - if (!twin_drm_apply_config(tx)) { - log_error("Failed to apply configurations to the DRM driver"); - goto bail_vt_fd; - } - - const twin_put_span_t drm_put_spans[] = { - _twin_drm_put_span16, - _twin_drm_put_span24, - _twin_drm_put_span32, - }; - - /* Create TWIN screen */ - ctx->screen = twin_screen_create(tx->width, tx->height, NULL, - drm_put_spans[tx->bpp / 8 - 2], ctx); - if (!ctx->screen) { - log_error("Failed to create screen"); - goto bail_vt_fd; - } - - /* Create Linux input system object */ - tx->input = twin_linux_input_create(ctx->screen); - if (!tx->input) { - log_error("Failed to create Linux input system object"); - goto bail_screen; + tx->fd = open(path, O_RDWR); + if (tx->fd < 0) { + log_error("Failed to open %s", path); + goto bail; } - - /* Setup file handler and work functions */ - twin_set_work(twin_drm_work, TWIN_WORK_REDISPLAY, ctx); - - /* Register a callback function to handle damaged rendering */ - twin_screen_register_damaged(ctx->screen, twin_drm_update_damage, ctx); - + uint64_t has_dumb; + if (drmGetCap(tx->fd, DRM_CAP_DUMB_BUFFER, &has_dumb) < 0 || !has_dumb) { + log_error("DRM device '%s' does not support dumb buffers", path); + goto bail_fd; + } + if (!twin_linux_fb_take_vt(&tx->fb)) + goto bail_fd; + if (!twin_drm_setup(tx) || !twin_linux_fb_start(ctx, tx->mode.hdisplay, + tx->mode.vdisplay, tx->bpp)) + goto bail_drm; return ctx; -bail_screen: - twin_screen_destroy(ctx->screen); -bail_vt_fd: - /* Restore VT mode before closing */ - ioctl(tx->vt_fd, VT_SETMODE, &tx->old_vtm); - twin_vt_mode(tx->vt_fd, KD_TEXT); - close(tx->vt_fd); -bail_dri_dev_fd: - close(tx->drm_dri_fd); +bail_drm: + twin_drm_teardown(tx); /* closes the device */ + twin_linux_fb_release_vt(&tx->fb); + goto bail; +bail_fd: + close(tx->fd); bail: free(ctx->priv); free(ctx); return NULL; } -static void twin_drm_configure(twin_context_t *ctx) -{ - int width, height; - twin_drm_t *tx = PRIV(ctx); - twin_drm_get_screen_size(tx, &width, &height); - twin_screen_resize(SCREEN(ctx), width, height); -} - static void twin_drm_exit(twin_context_t *ctx) { if (!ctx) return; - - twin_drm_t *tx = PRIV(ctx); - - /* Restore VT mode before cleanup */ - if (tx->vt_fd >= 0) { - ioctl(tx->vt_fd, VT_SETMODE, &tx->old_vtm); - twin_vt_mode(tx->vt_fd, KD_TEXT); - } - - drmModeFreeCrtc(CRTC(tx)); - destroy_fb(tx); - drmModeFreeConnector(CONN(tx)); - drmModeFreeResources(RES(tx)); - twin_linux_input_destroy(tx->input); - close(tx->vt_fd); - close(tx->drm_dri_fd); + twin_drm_t *tx = ctx->priv; + twin_drm_teardown(tx); + twin_linux_fb_stop(&tx->fb); free(ctx->priv); free(ctx); } -/* Poll function for DRM backend - * The DRM backend uses linux_input background thread for event handling, - * so this poll function just returns true to continue the main loop. - */ -static bool twin_drm_poll(twin_context_t *ctx maybe_unused) -{ - return true; -} - -static void twin_drm_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - if (init_callback) - init_callback(ctx); - - while (twin_dispatch_once(ctx)) - ; -} - -/* Register the Linux DRM backend */ - const twin_backend_t g_twin_backend = { .init = twin_drm_init, - .configure = twin_drm_configure, - .poll = twin_drm_poll, - .start = twin_drm_start, + .poll = twin_linux_fb_poll, .exit = twin_drm_exit, }; diff --git a/backend/fbdev.c b/backend/fbdev.c index 4f754cf7..0d4d324a 100644 --- a/backend/fbdev.c +++ b/backend/fbdev.c @@ -12,220 +12,84 @@ #include #include -#include "linux_input.h" -#include "linux_vt.h" -#include "twin_private.h" +#include "linux_fb.h" #define FBDEV_NAME "FRAMEBUFFER" #define FBDEV_DEFAULT "/dev/fb0" -#define SCREEN(x) ((twin_context_t *) x)->screen -#define PRIV(x) ((twin_fbdev_t *) ((twin_context_t *) x)->priv) typedef struct { - twin_screen_t *screen; - - /* Linux input system */ - void *input; - - /* Linux virtual terminal (VT) */ - int vt_fd; - bool vt_active; - struct vt_mode old_vtm; - - /* Linux framebuffer */ + twin_linux_fb_t fb; /* first: the shared core reads it from priv */ int fb_fd; struct fb_var_screeninfo fb_var; - struct fb_fix_screeninfo fb_fix; - uint16_t cmap[3][256]; - uint8_t *fb_base; - size_t fb_len; - - /* Initialization state */ - bool first_run; /* Track first work queue execution for initial damage */ } twin_fbdev_t; -/* color conversion */ -#define ARGB32_TO_RGB565_PERLINE(dest, pixels, width) \ - do { \ - uint16_t *_dest = (uint16_t *) dest; \ - for (int i = 0; i < width; i++) \ - _dest[i] = ((pixels[i] & 0x00f80000) >> 8) | \ - ((pixels[i] & 0x0000fc00) >> 5) | \ - ((pixels[i] & 0x000000f8) >> 3); \ - } while (0) - -/* Requires validation in 24-bit per pixel environments */ -#define ARGB32_TO_RGB888_PERLINE(dest, pixels, width) \ - do { \ - uint8_t *_dest = (uint8_t *) dest; \ - for (int i = 0; i < width; i++) { \ - _dest[i * 3] = pixels[i] & 0xFF; \ - _dest[i * 3 + 1] = ((pixels[i] & 0x0000FF00) >> 8); \ - _dest[i * 3 + 2] = ((pixels[i] & 0x00FF0000) >> 16); \ - } \ - } while (0) - -#define ARGB32_TO_ARGB32_PERLINE(dest, pixels, width) \ - memcpy((uint32_t *) dest, pixels, width * 4) - -#define FBDEV_PUT_SPAN_IMPL(bpp, op) \ - static void _twin_fbdev_put_span##bpp( \ - twin_coord_t left, twin_coord_t top, twin_coord_t right, \ - twin_argb32_t *pixels, void *closure) \ - { \ - uintptr_t dest; \ - twin_fbdev_t *tx = PRIV(closure); \ - off_t off = (bpp / 8) * left + top * tx->fb_fix.line_length; \ - dest = (uintptr_t) tx->fb_base + off; \ - twin_coord_t width = right - left; \ - op(dest, pixels, width); \ - } - -FBDEV_PUT_SPAN_IMPL(16, ARGB32_TO_RGB565_PERLINE) -FBDEV_PUT_SPAN_IMPL(24, ARGB32_TO_RGB888_PERLINE) -FBDEV_PUT_SPAN_IMPL(32, ARGB32_TO_ARGB32_PERLINE) - -static void twin_fbdev_get_screen_size(twin_fbdev_t *tx, - int *width, - int *height) +static bool _channels(const struct fb_var_screeninfo *v, + int r, + int rl, + int g, + int gl, + int b, + int bl) { - struct fb_var_screeninfo info; - ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &info); - *width = info.xres; - *height = info.yres; + return v->red.offset == (unsigned) r && v->red.length == (unsigned) rl && + v->green.offset == (unsigned) g && + v->green.length == (unsigned) gl && v->blue.offset == (unsigned) b && + v->blue.length == (unsigned) bl; } -static inline bool twin_fbdev_is_rgb565(twin_fbdev_t *tx) +/* Set the mode, check the pixel layout and map the framebuffer */ +static bool twin_fbdev_map(twin_linux_fb_t *fb) { - return tx->fb_var.red.offset == 11 && tx->fb_var.red.length == 5 && - tx->fb_var.green.offset == 5 && tx->fb_var.green.length == 6 && - tx->fb_var.blue.offset == 0 && tx->fb_var.blue.length == 5; -} + twin_fbdev_t *tx = (twin_fbdev_t *) fb; -static inline bool twin_fbdev_is_rgb888_or_argb32(twin_fbdev_t *tx) -{ - return tx->fb_var.red.offset == 16 && tx->fb_var.red.length == 8 && - tx->fb_var.green.offset == 8 && tx->fb_var.green.length == 8 && - tx->fb_var.blue.offset == 0 && tx->fb_var.blue.length == 8; -} - -static bool twin_fbdev_apply_config(twin_fbdev_t *tx) -{ - /* Read changable information of the framebuffer */ - if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) == -1) { + if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) < 0) { log_error("Failed to get framebuffer information"); return false; } - - /* Set the virtual screen size to be the same as the physical screen */ + /* The virtual screen is the physical one: no panning */ tx->fb_var.xres_virtual = tx->fb_var.xres; tx->fb_var.yres_virtual = tx->fb_var.yres; - if (ioctl(tx->fb_fd, FBIOPUT_VSCREENINFO, &tx->fb_var) < 0) { + if (ioctl(tx->fb_fd, FBIOPUT_VSCREENINFO, &tx->fb_var) < 0 || + ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) < 0) { log_error("Failed to set framebuffer mode"); return false; } - /* Read changable information of the framebuffer again */ - if (ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &tx->fb_var) < 0) { - log_error("Failed to get framebuffer information"); - return false; - } - - /* Examine the framebuffer format */ + bool ok; switch (tx->fb_var.bits_per_pixel) { - case 16: /* RGB565 */ - if (!twin_fbdev_is_rgb565(tx)) { - log_error("Invalid framebuffer format for 16 bpp"); - return false; - } + case 16: + ok = _channels(&tx->fb_var, 11, 5, 5, 6, 0, 5); break; - case 24: /* RGB888 */ - case 32: /* ARGB32 */ - if (!twin_fbdev_is_rgb888_or_argb32(tx)) { - log_error("Invalid framebuffer format for %u bpp", - tx->fb_var.bits_per_pixel); - return false; - } + case 24: + case 32: + ok = _channels(&tx->fb_var, 16, 8, 8, 8, 0, 8); break; default: - log_error("Unsupported bits per pixel: %d", tx->fb_var.bits_per_pixel); + ok = false; + break; + } + if (!ok) { + log_error("Unsupported framebuffer format: %u bpp", + tx->fb_var.bits_per_pixel); return false; } - /* Read unchangable information of the framebuffer */ - ioctl(tx->fb_fd, FBIOGET_FSCREENINFO, &tx->fb_fix); - - /* Align the framebuffer memory address with the page size */ - off_t pgsize = getpagesize(); - off_t start = (off_t) tx->fb_fix.smem_start & (pgsize - 1); - - /* Round up the framebuffer memory size to match the page size */ - tx->fb_len = start + (size_t) tx->fb_fix.smem_len + (pgsize - 1); - tx->fb_len &= ~(pgsize - 1); - - /* Map framebuffer device to the virtual memory */ - tx->fb_base = mmap(NULL, tx->fb_len, PROT_READ | PROT_WRITE, MAP_SHARED, + struct fb_fix_screeninfo fix; + if (ioctl(tx->fb_fd, FBIOGET_FSCREENINFO, &fix) < 0) { + log_error("Failed to get framebuffer layout"); + return false; + } + /* The mapping starts on a page boundary and covers whole pages */ + size_t pgsize = (size_t) getpagesize(); + size_t start = (size_t) fix.smem_start & (pgsize - 1); + fb->fb_len = (start + fix.smem_len + pgsize - 1) & ~(pgsize - 1); + fb->stride = fix.line_length; + fb->fb_base = mmap(NULL, fb->fb_len, PROT_READ | PROT_WRITE, MAP_SHARED, tx->fb_fd, 0); - if (tx->fb_base == MAP_FAILED) { + if (fb->fb_base == MAP_FAILED) { log_error("Failed to mmap framebuffer"); return false; } - - return true; -} - -static void twin_fbdev_damage(twin_fbdev_t *tx) -{ - int width, height; - twin_fbdev_get_screen_size(tx, &width, &height); - twin_screen_damage(tx->screen, 0, 0, width, height); -} - -static bool twin_fbdev_update_damage(void *closure) -{ - twin_fbdev_t *tx = PRIV(closure); - twin_screen_t *screen = SCREEN(closure); - - if (!tx->vt_active && (tx->fb_base != MAP_FAILED) && - twin_screen_damaged(screen)) - twin_screen_update(screen); - - return true; -} - -static bool twin_fbdev_work(void *closure) -{ - twin_fbdev_t *tx = PRIV(closure); - twin_screen_t *screen = SCREEN(closure); - - /* Mark entire screen as damaged on first run to ensure initial rendering. - * This is necessary because the screen content is not automatically - * rendered when the application starts. Using per-context state allows - * proper reinitialization if the backend is torn down and recreated. - */ - if (tx->first_run) { - tx->first_run = false; - twin_screen_damage(screen, 0, 0, screen->width, screen->height); - } - - if (tx->vt_active && (tx->fb_base != MAP_FAILED)) { - /* Unmap the fbdev */ - munmap(tx->fb_base, tx->fb_len); - tx->fb_base = MAP_FAILED; - } - - if (!tx->vt_active && (tx->fb_base == MAP_FAILED)) { - /* Restore the fbdev settings */ - if (!twin_fbdev_apply_config(tx)) - log_error("Failed to apply configurations to the fbdev"); - - /* Mark entire screen for refresh */ - twin_screen_damage(screen, 0, 0, screen->width, screen->height); - } - - if (!tx->vt_active && twin_screen_damaged(screen)) - twin_screen_update(screen); - return true; } @@ -241,90 +105,33 @@ twin_context_t *twin_fbdev_init(int width maybe_unused, int height maybe_unused) twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); if (!ctx) return NULL; - ctx->priv = calloc(1, sizeof(twin_fbdev_t)); - if (!ctx->priv) + twin_fbdev_t *tx = ctx->priv = calloc(1, sizeof(twin_fbdev_t)); + if (!tx) goto bail; + tx->fb.fb_base = MAP_FAILED; + tx->fb.remap = twin_fbdev_map; - twin_fbdev_t *tx = ctx->priv; - - /* Initialize first_run flag for initial screen damage */ - tx->first_run = true; - - /* Open the framebuffer device */ tx->fb_fd = open(fbdev_path, O_RDWR); - if (tx->fb_fd == -1) { + if (tx->fb_fd < 0) { log_error("Failed to open %s", fbdev_path); goto bail; } - - /* Set up virtual terminal environment */ - if (!twin_vt_setup(&tx->vt_fd, &tx->old_vtm, &tx->vt_active)) { + if (!twin_linux_fb_take_vt(&tx->fb)) goto bail_fb_fd; - } - - /* Set up signal handlers for switching TTYs */ - twin_vt_setup_signal_handler(); - - /* Apply configurations to the framebuffer device */ - if (!twin_fbdev_apply_config(tx)) { - log_error("Failed to apply configurations to the framebuffer device"); - goto bail_vt_fd; - } - - /* Examine if framebuffer mapping is valid */ - if (tx->fb_base == MAP_FAILED) { - log_error("Failed to map framebuffer memory"); - goto bail_vt_fd; - } - - /* Use actual framebuffer resolution instead of caller-supplied dimensions. - * The physical display size is authoritative -- the caller's width/height - * are only hints suitable for resizable backends like SDL. - * Read directly from fb_var which was already validated by - * twin_fbdev_apply_config() above -- no redundant ioctl needed. - */ - int fb_width = tx->fb_var.xres; - int fb_height = tx->fb_var.yres; - - const twin_put_span_t fbdev_put_spans[] = { - _twin_fbdev_put_span16, - _twin_fbdev_put_span24, - _twin_fbdev_put_span32, - }; - /* Create TWIN screen */ - ctx->screen = twin_screen_create( - fb_width, fb_height, NULL, - fbdev_put_spans[tx->fb_var.bits_per_pixel / 8 - 2], ctx); - if (!ctx->screen) { - log_error("Failed to create screen"); - goto bail_fb_unmap; - } - - /* Create Linux input system object */ - tx->input = twin_linux_input_create(ctx->screen); - if (!tx->input) { - log_error("Failed to create Linux input system object"); - goto bail_screen; - } - - /* Setup file handler and work functions */ - twin_set_work(twin_fbdev_work, TWIN_WORK_REDISPLAY, ctx); - - /* Register a callback function to handle damaged rendering */ - twin_screen_register_damaged(ctx->screen, twin_fbdev_update_damage, ctx); - + if (!twin_fbdev_map(&tx->fb)) + goto bail_vt; + + /* The display's own resolution wins over the caller's hint, which + * only resizable backends such as SDL honor */ + if (!twin_linux_fb_start(ctx, tx->fb_var.xres, tx->fb_var.yres, + tx->fb_var.bits_per_pixel)) + goto bail_unmap; return ctx; -bail_screen: - twin_screen_destroy(ctx->screen); -bail_fb_unmap: - if (tx->fb_base != MAP_FAILED) - munmap(tx->fb_base, tx->fb_len); -bail_vt_fd: - /* Restore VT mode before closing */ - ioctl(tx->vt_fd, VT_SETMODE, &tx->old_vtm); - ioctl(tx->vt_fd, KDSETMODE, KD_TEXT); - close(tx->vt_fd); +bail_unmap: + twin_linux_fb_unmap(&tx->fb); +bail_vt: + twin_linux_fb_release_vt(&tx->fb); bail_fb_fd: close(tx->fb_fd); bail: @@ -333,66 +140,20 @@ twin_context_t *twin_fbdev_init(int width maybe_unused, int height maybe_unused) return NULL; } -static void twin_fbdev_configure(twin_context_t *ctx) -{ - int width, height; - twin_fbdev_t *tx = ctx->priv; - twin_fbdev_get_screen_size(tx, &width, &height); - twin_screen_resize(ctx->screen, width, height); -} - static void twin_fbdev_exit(twin_context_t *ctx) { if (!ctx) return; - - twin_fbdev_t *tx = PRIV(ctx); - - /* Restore VT mode before cleanup */ - if (tx->vt_fd >= 0) { - ioctl(tx->vt_fd, VT_SETMODE, &tx->old_vtm); - ioctl(tx->vt_fd, KDSETMODE, KD_TEXT); - } - - munmap(tx->fb_base, tx->fb_len); - twin_linux_input_destroy(tx->input); - close(tx->vt_fd); + twin_fbdev_t *tx = ctx->priv; + twin_linux_fb_stop(&tx->fb); + twin_linux_fb_unmap(&tx->fb); close(tx->fb_fd); free(ctx->priv); free(ctx); } -/* Poll function for fbdev backend - * The fbdev backend uses linux_input background thread for event handling, - * so this poll function just returns true to continue the main loop. - */ -static bool twin_fbdev_poll(twin_context_t *ctx maybe_unused) -{ - return true; -} - -/* Start function for fbdev backend - * Note: fbdev uses Linux input system with background thread for events, - * so we use the standard dispatcher for work queue and timeout processing. - */ -static void twin_fbdev_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - if (init_callback) - init_callback(ctx); - - /* Use standard dispatcher to ensure work queue and timeouts run. - * Events are handled by linux_input background thread. - */ - while (twin_dispatch_once(ctx)) - ; -} - -/* Register the Linux framebuffer backend */ const twin_backend_t g_twin_backend = { .init = twin_fbdev_init, - .configure = twin_fbdev_configure, - .poll = twin_fbdev_poll, - .start = twin_fbdev_start, + .poll = twin_linux_fb_poll, .exit = twin_fbdev_exit, }; diff --git a/backend/headless-shm.h b/backend/headless-shm.h index e7d6dc3d..e2efb5fa 100644 --- a/backend/headless-shm.h +++ b/backend/headless-shm.h @@ -7,6 +7,7 @@ #ifndef _TWIN_HEADLESS_SHM_H_ #define _TWIN_HEADLESS_SHM_H_ +#include #include #include @@ -14,7 +15,7 @@ #define TWIN_HEADLESS_SHM_NAME "/mado-headless-shm" #define TWIN_HEADLESS_MAGIC 0x5457494E /* "TWIN" */ -#define TWIN_HEADLESS_VERSION 1 +#define TWIN_HEADLESS_VERSION 2 #define TWIN_HEADLESS_MAX_EVENTS 64 typedef enum { @@ -32,12 +33,17 @@ typedef struct { twin_coord_t x, y; twin_count_t button; } pointer; + struct { + twin_ucs4_t ucs4; + } ucs4; } u; } twin_headless_event_t; typedef struct { uint32_t magic; uint32_t version; + /* Backend process: tells a live object from one a crash left behind */ + int32_t owner_pid; /* Screen info */ twin_coord_t width, height; @@ -72,4 +78,21 @@ typedef struct { /* twin_argb32_t framebuffer[width * height]; */ } twin_headless_shm_t; +/* + * Bytes for the header plus @frames frames of @width x @height ARGB32, or 0 + * when that overflows. The backend sizes the object with it and clients + * check what they mapped against it, so both agree on the layout. + */ +static inline size_t twin_headless_shm_bytes(size_t width, + size_t height, + size_t frames) +{ + size_t n; + if (__builtin_mul_overflow(width, height, &n) || + __builtin_mul_overflow(n, frames * sizeof(twin_argb32_t), &n) || + __builtin_add_overflow(n, sizeof(twin_headless_shm_t), &n)) + return 0; + return n; +} + #endif /* _TWIN_HEADLESS_SHM_H_ */ diff --git a/backend/headless.c b/backend/headless.c index e295b206..96a3c21c 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -42,6 +42,7 @@ typedef struct { int shm_fd; size_t shm_size; uint32_t last_cmd_seq; + twin_coord_t dirty_top, dirty_bottom; /* rows scanned out this update */ uint64_t last_frame_time; uint32_t frame_count; bool running; @@ -88,11 +89,13 @@ static void _twin_headless_put_begin(twin_coord_t left, twin_coord_t bottom, void *closure) { + twin_headless_t *tx = PRIV(closure); (void) left; - (void) top; (void) right; - (void) bottom; - (void) closure; + if (top < tx->dirty_top) + tx->dirty_top = top; + if (bottom > tx->dirty_bottom) + tx->dirty_bottom = bottom; } static void _twin_headless_put_span(twin_coord_t left, @@ -166,48 +169,73 @@ static void _twin_headless_inject_events(twin_context_t *ctx) if (!shm) return; - while (shm->event_read_idx != shm->event_write_idx) { - uint32_t idx = shm->event_read_idx % TWIN_HEADLESS_MAX_EVENTS; - twin_headless_event_t *evt = &shm->events[idx]; + /* The queue lives in memory another process writes: never trust its + * indices beyond one queue's worth, and copy each event before use. */ + uint32_t read_idx = shm->event_read_idx; + uint32_t pending = shm->event_write_idx - read_idx; + if (pending > TWIN_HEADLESS_MAX_EVENTS) { + fprintf(stderr, + "Headless: event queue out of sync, dropping %u events\n", + pending); + shm->event_read_idx = read_idx + pending; + return; + } + + while (pending--) { + twin_headless_event_t evt = + shm->events[read_idx % TWIN_HEADLESS_MAX_EVENTS]; twin_event_t twin_evt; memset(&twin_evt, 0, sizeof(twin_evt)); - twin_evt.kind = evt->kind; + twin_evt.kind = evt.kind; - switch (evt->kind) { + switch (evt.kind) { case TwinEventButtonDown: case TwinEventButtonUp: case TwinEventMotion: - twin_evt.u.pointer.screen_x = evt->u.pointer.x; - twin_evt.u.pointer.screen_y = evt->u.pointer.y; - twin_evt.u.pointer.button = evt->u.pointer.button; + twin_evt.u.pointer.screen_x = evt.u.pointer.x; + twin_evt.u.pointer.screen_y = evt.u.pointer.y; + twin_evt.u.pointer.button = evt.u.pointer.button; _twin_headless_track_event(tx, "mouse"); + twin_screen_dispatch(screen, &twin_evt); + break; + + case TwinEventUcs4: + twin_evt.u.ucs4.ucs4 = evt.u.ucs4.ucs4; + _twin_headless_track_event(tx, "key"); + twin_screen_dispatch(screen, &twin_evt); break; default: + /* Only input may be injected: internal kinds such as Paint, + * Configure or Destroy are not the control tool's to send */ _twin_headless_track_event(tx, "other"); break; } - - twin_screen_dispatch(screen, &twin_evt); - shm->event_read_idx++; + shm->event_read_idx = ++read_idx; } } -static void twin_headless_update_damage(void *closure) +static bool twin_headless_update_damage(void *closure) { twin_context_t *ctx = closure; twin_screen_t *screen = ctx->screen; twin_headless_t *tx = ctx->priv; if (!twin_screen_damaged(screen)) - return; + return true; + tx->dirty_top = screen->height; + tx->dirty_bottom = 0; twin_screen_update(screen); - /* Flip buffers - copy back buffer to front buffer */ - size_t buffer_size = screen->width * screen->height * sizeof(twin_argb32_t); - memcpy(tx->front_buffer, tx->back_buffer, buffer_size); + /* Publish only the rows that changed */ + if (tx->dirty_top < tx->dirty_bottom) { + size_t row = (size_t) screen->width * sizeof(twin_argb32_t); + memcpy((uint8_t *) tx->front_buffer + tx->dirty_top * row, + (uint8_t *) tx->back_buffer + tx->dirty_top * row, + (size_t) (tx->dirty_bottom - tx->dirty_top) * row); + } /* Update frame statistics */ uint64_t now = _twin_headless_get_timestamp(); @@ -224,14 +252,7 @@ static void twin_headless_update_damage(void *closure) tx->last_frame_time = now; tx->frame_count = 0; } -} - -static void twin_headless_configure(twin_context_t *ctx) -{ - twin_screen_t *screen = ctx->screen; - - screen->put_begin = _twin_headless_put_begin; - screen->put_span = _twin_headless_put_span; + return true; } static bool twin_headless_poll(twin_context_t *ctx) @@ -247,7 +268,11 @@ static bool twin_headless_poll(twin_context_t *ctx) /* Inject events from control tool */ _twin_headless_inject_events(ctx); - /* Screen update is handled by damage callback, no need to call here */ + /* Idle: sleep until the next timeout, but stay responsive to the + * control tool. Damage is scanned out by the redisplay work. */ + twin_time_t wait = _twin_idle_timeout(ctx->screen, 5); + if (wait > 0) + usleep((useconds_t) wait * 1000); /* Update heartbeat timestamp periodically to show backend is alive */ if (tx->shm) { @@ -280,6 +305,30 @@ static void twin_headless_exit(twin_context_t *ctx) free(ctx); } +/* Whether the existing shared memory object names a backend still running */ +static bool _twin_headless_shm_live(void) +{ + bool live = false; + struct stat st; + int fd = shm_open(TWIN_HEADLESS_SHM_NAME, O_RDONLY, 0); + if (fd < 0) + return false; + if (fstat(fd, &st) == 0 && + (size_t) st.st_size >= sizeof(twin_headless_shm_t)) { + twin_headless_shm_t *shm = + mmap(NULL, sizeof(*shm), PROT_READ, MAP_SHARED, fd, 0); + if (shm != MAP_FAILED) { + pid_t pid = shm->owner_pid; + live = shm->magic == TWIN_HEADLESS_MAGIC && + shm->version == TWIN_HEADLESS_VERSION && pid > 0 && + (kill(pid, 0) == 0 || errno == EPERM); + munmap(shm, sizeof(*shm)); + } + } + close(fd); + return live; +} + twin_context_t *twin_headless_init(int width, int height) { twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); @@ -296,13 +345,28 @@ twin_context_t *twin_headless_init(int width, int height) tx->running = true; tx->shm_fd = -1; - /* Calculate shared memory size (header + 2 buffers for double buffering) */ - size_t header_size = sizeof(twin_headless_shm_t); - size_t fb_size = width * height * sizeof(twin_argb32_t); - tx->shm_size = header_size + (fb_size * 2); /* Two buffers */ + /* Header plus two buffers for double buffering */ + tx->shm_size = twin_headless_shm_bytes(width, height, 2); + if (!tx->shm_size) + goto error; - /* Create shared memory */ - tx->shm_fd = shm_open(TWIN_HEADLESS_SHM_NAME, O_CREAT | O_RDWR, 0666); + /* Create shared memory, readable and writable by this user only. + * O_EXCL makes sure the backend never adopts an object somebody else + * created; one left by a crashed run is removed and created afresh, + * but a running backend's is left alone. */ + tx->shm_fd = + shm_open(TWIN_HEADLESS_SHM_NAME, O_CREAT | O_EXCL | O_RDWR, 0600); + if (tx->shm_fd < 0 && errno == EEXIST) { + if (_twin_headless_shm_live()) { + fprintf(stderr, + "Shared memory %s belongs to a running headless backend\n", + TWIN_HEADLESS_SHM_NAME); + goto error; + } + shm_unlink(TWIN_HEADLESS_SHM_NAME); + tx->shm_fd = + shm_open(TWIN_HEADLESS_SHM_NAME, O_CREAT | O_EXCL | O_RDWR, 0600); + } if (tx->shm_fd < 0) { fprintf(stderr, "Failed to create shared memory: %s\n", strerror(errno)); @@ -331,6 +395,7 @@ twin_context_t *twin_headless_init(int width, int height) memset(tx->shm, 0, tx->shm_size); tx->shm->magic = TWIN_HEADLESS_MAGIC; tx->shm->version = TWIN_HEADLESS_VERSION; + tx->shm->owner_pid = getpid(); tx->shm->width = width; tx->shm->height = height; @@ -350,7 +415,8 @@ twin_context_t *twin_headless_init(int width, int height) tx->frame_count = 0; /* Set framebuffer pointers */ - tx->front_buffer = (twin_argb32_t *) ((char *) tx->shm + header_size); + tx->front_buffer = + (twin_argb32_t *) ((char *) tx->shm + sizeof(twin_headless_shm_t)); tx->back_buffer = tx->front_buffer + (width * height); tx->framebuffer = tx->back_buffer; /* Start rendering to back buffer */ @@ -360,7 +426,9 @@ twin_context_t *twin_headless_init(int width, int height) if (!ctx->screen) goto error; - twin_screen_register_damaged(ctx->screen, twin_headless_update_damage, ctx); + /* Scan out once per loop iteration, after the paint work, so damage + * coalesces and a window is never shown before it is painted */ + twin_set_work(twin_headless_update_damage, TWIN_WORK_REDISPLAY, ctx); return ctx; @@ -376,14 +444,7 @@ twin_context_t *twin_headless_init(int width, int height) return NULL; } -#else /* !HAVE_POSIX_SHM - Bare-metal stub implementation */ -/* Provides minimal no-op backend for embedded systems without POSIX support */ - -static void twin_headless_config_dummy(twin_context_t *ctx) -{ - (void) ctx; -} - +#else /* !HAVE_POSIX_SHM - Bare-metal stub implementation */ static bool twin_headless_poll_dummy(twin_context_t *ctx) { (void) ctx; @@ -402,41 +463,17 @@ static twin_context_t *twin_headless_init_dummy(int width, int height) return NULL; } -static void twin_headless_start_dummy(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - (void) ctx; - (void) init_callback; -} #endif /* HAVE_POSIX_SHM */ -#ifdef HAVE_POSIX_SHM -/* Start function for headless backend */ -static void twin_headless_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - if (init_callback) - init_callback(ctx); - - /* Use standard dispatcher to ensure work queue and timeouts run */ - while (twin_dispatch_once(ctx)) - ; -} -#endif - /* Register the headless backend */ const twin_backend_t g_twin_backend = { #ifdef HAVE_POSIX_SHM .init = twin_headless_init, - .configure = twin_headless_configure, .poll = twin_headless_poll, - .start = twin_headless_start, .exit = twin_headless_exit, #else .init = twin_headless_init_dummy, - .configure = twin_headless_config_dummy, .poll = twin_headless_poll_dummy, - .start = twin_headless_start_dummy, .exit = twin_headless_exit_dummy, #endif }; diff --git a/backend/linux_fb.h b/backend/linux_fb.h new file mode 100644 index 00000000..6792bd5b --- /dev/null +++ b/backend/linux_fb.h @@ -0,0 +1,209 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Shared core of the Linux framebuffer backends (fbdev and DRM): the + * virtual terminal, input, the mapped framebuffer and its span writers, + * the redisplay work and the idle wait. A backend's private struct embeds + * twin_linux_fb_t as its first member and supplies how to map the + * framebuffer again after a VT switch, plus an optional flush. + */ + +#ifndef _LINUX_FB_H_ +#define _LINUX_FB_H_ + +#include +#include + +#include "linux_input.h" +#include "linux_vt.h" +#include "twin_private.h" + +typedef struct twin_linux_fb { + twin_linux_input_t *input; + + int vt_fd; + volatile sig_atomic_t vt_away; /* switched away: set by the VT signal */ + struct vt_mode old_vtm; + + uint8_t *fb_base; /* MAP_FAILED while switched away */ + size_t fb_len; + size_t stride; /* bytes per row */ + + /* Map the framebuffer again on return to the VT */ + bool (*remap)(struct twin_linux_fb *fb); + /* Push a finished frame to the display; NULL when writes show */ + void (*flush)(struct twin_linux_fb *fb); +} twin_linux_fb_t; + +#define LINUX_FB(ctx) ((twin_linux_fb_t *) ((twin_context_t *) (ctx))->priv) + +static inline uint8_t *_twin_linux_fb_row(void *closure, + twin_coord_t left, + twin_coord_t top, + int bytes) +{ + twin_linux_fb_t *fb = LINUX_FB(closure); + return fb->fb_base + (size_t) top * fb->stride + (size_t) left * bytes; +} + +static void _twin_linux_fb_span16(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + uint16_t *d = (uint16_t *) _twin_linux_fb_row(closure, left, top, 2); + for (twin_coord_t i = 0; i < right - left; i++) + d[i] = twin_argb32_to_rgb16(pixels[i]); +} + +/* Blue first, as little-endian RGB888 framebuffers lay it out */ +static void _twin_linux_fb_span24(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + uint8_t *d = _twin_linux_fb_row(closure, left, top, 3); + for (twin_coord_t i = 0; i < right - left; i++, d += 3) { + d[0] = (uint8_t) pixels[i]; + d[1] = (uint8_t) (pixels[i] >> 8); + d[2] = (uint8_t) (pixels[i] >> 16); + } +} + +static void _twin_linux_fb_span32(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + memcpy(_twin_linux_fb_row(closure, left, top, 4), pixels, + (size_t) (right - left) * sizeof(*pixels)); +} + +/* Span writer for @bpp (16, 24 or 32), NULL for any other depth */ +static inline twin_put_span_t twin_linux_fb_span(int bpp) +{ + switch (bpp) { + case 16: + return _twin_linux_fb_span16; + case 24: + return _twin_linux_fb_span24; + case 32: + return _twin_linux_fb_span32; + default: + return NULL; + } +} + +/* Drop the mapping, if any */ +static inline void twin_linux_fb_unmap(twin_linux_fb_t *fb) +{ + if (fb->fb_base != MAP_FAILED) + munmap(fb->fb_base, fb->fb_len); + fb->fb_base = MAP_FAILED; +} + +static bool _twin_linux_fb_work(void *closure) +{ + twin_linux_fb_t *fb = LINUX_FB(closure); + twin_screen_t *screen = ((twin_context_t *) closure)->screen; + + if (fb->vt_away) + twin_linux_fb_unmap(fb); + + if (!fb->vt_away && fb->fb_base == MAP_FAILED) { + if (!fb->remap(fb)) + log_error("Failed to map the framebuffer again"); + /* Whoever had the VT drew over everything */ + twin_screen_damage(screen, 0, 0, screen->width, screen->height); + } + + /* No mapping (restoring it failed): keep the damage for the next try */ + if (!fb->vt_away && fb->fb_base != MAP_FAILED && + twin_screen_damaged(screen)) { + twin_screen_update(screen); + if (fb->flush) + fb->flush(fb); + } + return true; +} + +/* Take the VT for graphics; on failure nothing is left to undo */ +static inline bool twin_linux_fb_take_vt(twin_linux_fb_t *fb) +{ + if (!twin_vt_setup(&fb->vt_fd, &fb->old_vtm, &fb->vt_away)) + return false; + twin_vt_setup_signal_handler(); + return true; +} + +static inline void twin_linux_fb_release_vt(twin_linux_fb_t *fb) +{ + twin_vt_restore(fb->vt_fd, &fb->old_vtm); + close(fb->vt_fd); +} + +/* Create the screen over the mapped framebuffer, input and redisplay + * work; on failure nothing created here is left behind */ +static inline bool twin_linux_fb_start(twin_context_t *ctx, + int width, + int height, + int bpp) +{ + twin_linux_fb_t *fb = LINUX_FB(ctx); + twin_put_span_t span = twin_linux_fb_span(bpp); + if (!span) { + log_error("Unsupported bits per pixel: %d", bpp); + return false; + } + ctx->screen = twin_screen_create(width, height, NULL, span, ctx); + if (!ctx->screen) { + log_error("Failed to create screen"); + return false; + } + fb->input = twin_linux_input_create(ctx->screen); + if (!fb->input) { + log_error("Failed to create Linux input system object"); + twin_screen_destroy(ctx->screen); + ctx->screen = NULL; + return false; + } + twin_set_work(_twin_linux_fb_work, TWIN_WORK_REDISPLAY, ctx); + return true; +} + +/* Undo twin_linux_fb_start() and give the VT back */ +static inline void twin_linux_fb_stop(twin_linux_fb_t *fb) +{ + twin_linux_input_destroy(fb->input); + twin_linux_fb_release_vt(fb); +} + +/* Retry period for a framebuffer that could not be mapped again */ +#define TWIN_LINUX_FB_RETRY_MS 250 + +/* Sleep in the input poll until input arrives or there is something to + * show; input is dispatched on this thread. While the framebuffer is + * gone, damage cannot be shown and does not end the wait: only timers, + * the VT signal, or a retry of a failed mapping do. */ +static bool twin_linux_fb_poll(twin_context_t *ctx) +{ + twin_linux_fb_t *fb = LINUX_FB(ctx); + twin_time_t wait; + if (fb->vt_away) + wait = _twin_idle_timeout(NULL, -1); + else if (fb->fb_base == MAP_FAILED) + wait = _twin_idle_timeout(NULL, TWIN_LINUX_FB_RETRY_MS); + else + wait = _twin_idle_timeout(ctx->screen, -1); + twin_linux_input_poll(fb->input, (int) wait); + return true; +} + +#endif /* _LINUX_FB_H_ */ diff --git a/backend/linux_input.c b/backend/linux_input.c index 7f3f1c7b..8dac6545 100644 --- a/backend/linux_input.c +++ b/backend/linux_input.c @@ -4,11 +4,14 @@ * All rights reserved. */ +/* ppoll() */ +#define _GNU_SOURCE +#include #include #include #include #include -#include +#include #include #include #include @@ -20,13 +23,6 @@ #define EVDEV_CNT_MAX 32 #define EVDEV_NAME_SIZE_MAX 50 -#define UDEV_RESERVED_CNT 1 -#define UDEV_EVDEV_FD_CNT (EVDEV_CNT_MAX + UDEV_RESERVED_CNT) - -struct evdev_info { - int idx; - int fd; -}; /* Coordinate smoothing configuration */ /* Weight for smoothing (0=off, 1-7=strength) */ @@ -39,13 +35,15 @@ struct evdev_info { #error "TWIN_INPUT_SMOOTH_WEIGHT must be in range [0, 7]" #endif -typedef struct { +struct twin_linux_input { twin_screen_t *screen; - pthread_t evdev_thread; - struct evdev_info evdevs[EVDEV_CNT_MAX]; - size_t evdev_cnt; - size_t udev_cnt; - int fd; + struct udev *udev; + struct udev_monitor *mon; + /* [0]: the udev monitor (fd -1 without one, which poll() skips), then + * one entry per open event device */ + struct pollfd pfds[1 + EVDEV_CNT_MAX]; + int evdev_idx[EVDEV_CNT_MAX]; /* /dev/input/eventN of pfds[1 + i] */ + int evdev_cnt; int btns; int x, y; int abs_x_min, abs_y_min; /* Minimum value for ABS_X/ABS_Y from device */ @@ -55,7 +53,7 @@ typedef struct { int smooth_x, smooth_y; /* Smoothed coordinates for ABS events */ bool smooth_initialized; /* Whether smoothing has been initialized */ #endif -} twin_linux_input_t; +}; static void check_mouse_bounds(twin_linux_input_t *tm) { @@ -105,97 +103,79 @@ static inline void smooth_abs_coords(twin_linux_input_t *tm, } #endif +/* Send the pointer state as an event of @kind */ +static void _pointer(twin_linux_input_t *tm, twin_event_kind_t kind) +{ + twin_event_t tev = {.kind = kind}; + check_mouse_bounds(tm); + tev.u.pointer.screen_x = tm->x; + tev.u.pointer.screen_y = tm->y; + tev.u.pointer.button = tm->btns; + twin_screen_dispatch(tm->screen, &tev); +} + +/* Map a device coordinate in [min, max] to [0, size - 1]; false for a + * device range that cannot be scaled */ +static bool _scale_abs(int value, int min, int max, int size, int *out) +{ + int range = max - min; + if (range <= 0) + return false; + *out = (int) (((int64_t) (value - min) * (size - 1)) / range); + return true; +} + static void twin_linux_input_events(struct input_event *ev, twin_linux_input_t *tm) { - /* TODO: twin_screen_dispatch() should be protect by mutex_lock, but the - * counterpart piece of code with mutex is not yet sure. - */ - - twin_event_t tev; + int v; switch (ev->type) { case EV_REL: - if (ev->code == REL_X) { + if (ev->code == REL_X) tm->x += ev->value; - check_mouse_bounds(tm); - tev.kind = TwinEventMotion; - tev.u.pointer.screen_x = tm->x; - tev.u.pointer.screen_y = tm->y; - tev.u.pointer.button = tm->btns; - twin_screen_dispatch(tm->screen, &tev); - } else if (ev->code == REL_Y) { + else if (ev->code == REL_Y) tm->y += ev->value; - check_mouse_bounds(tm); - tev.kind = TwinEventMotion; - tev.u.pointer.screen_x = tm->x; - tev.u.pointer.screen_y = tm->y; - tev.u.pointer.button = tm->btns; - twin_screen_dispatch(tm->screen, &tev); - } + else + break; + _pointer(tm, TwinEventMotion); break; case EV_ABS: - /* Scale absolute coordinates to screen resolution. - * The range is dynamically queried from each device using EVIOCGABS. - * Formula: scaled = ((value - min) * (screen_size - 1)) / (max - min) - * This correctly maps device coordinates [min, max] to screen [0, - * size-1] - */ if (ev->code == ABS_X) { - int range = tm->abs_x_max - tm->abs_x_min; - if (range > 0) { - int raw_x = ((int64_t) (ev->value - tm->abs_x_min) * - (tm->screen->width - 1)) / - range; -#if TWIN_INPUT_SMOOTH_WEIGHT > 0 - /* Apply smoothing to X axis, keep Y unchanged */ - smooth_abs_coords(tm, raw_x, tm->y); -#else - tm->x = raw_x; -#endif - check_mouse_bounds(tm); - tev.kind = TwinEventMotion; - tev.u.pointer.screen_x = tm->x; - tev.u.pointer.screen_y = tm->y; - tev.u.pointer.button = tm->btns; - twin_screen_dispatch(tm->screen, &tev); - } else { + if (!_scale_abs(ev->value, tm->abs_x_min, tm->abs_x_max, + tm->screen->width, &v)) { log_warn("Ignoring ABS_X event: invalid range [%d,%d]", tm->abs_x_min, tm->abs_x_max); + break; } - } else if (ev->code == ABS_Y) { - int range = tm->abs_y_max - tm->abs_y_min; - if (range > 0) { - int raw_y = ((int64_t) (ev->value - tm->abs_y_min) * - (tm->screen->height - 1)) / - range; #if TWIN_INPUT_SMOOTH_WEIGHT > 0 - /* Apply smoothing to Y axis, keep X unchanged */ - smooth_abs_coords(tm, tm->x, raw_y); + smooth_abs_coords(tm, v, tm->y); #else - tm->y = raw_y; + tm->x = v; #endif - check_mouse_bounds(tm); - tev.kind = TwinEventMotion; - tev.u.pointer.screen_x = tm->x; - tev.u.pointer.screen_y = tm->y; - tev.u.pointer.button = tm->btns; - twin_screen_dispatch(tm->screen, &tev); - } else { + } else if (ev->code == ABS_Y) { + if (!_scale_abs(ev->value, tm->abs_y_min, tm->abs_y_max, + tm->screen->height, &v)) { log_warn("Ignoring ABS_Y event: invalid range [%d,%d]", tm->abs_y_min, tm->abs_y_max); + break; } +#if TWIN_INPUT_SMOOTH_WEIGHT > 0 + smooth_abs_coords(tm, tm->x, v); +#else + tm->y = v; +#endif + } else { + break; } + _pointer(tm, TwinEventMotion); break; case EV_KEY: if (ev->code == BTN_LEFT) { tm->btns = ev->value > 0 ? 1 : 0; - tev.kind = tm->btns ? TwinEventButtonDown : TwinEventButtonUp; - tev.u.pointer.screen_x = tm->x; - tev.u.pointer.screen_y = tm->y; - tev.u.pointer.button = tm->btns; - twin_screen_dispatch(tm->screen, &tev); + _pointer(tm, tm->btns ? TwinEventButtonDown : TwinEventButtonUp); } + break; } } @@ -310,141 +290,109 @@ static void twin_linux_input_query_abs(int fd, twin_linux_input_t *tm) } } -static void twin_linux_edev_open(struct pollfd *pfds, twin_linux_input_t *tm) +/* Open the event devices present now, keeping those already open and + * closing those that went away */ +static void twin_linux_edev_open(twin_linux_input_t *tm) { - /* New event device list */ - struct evdev_info evdevs[EVDEV_CNT_MAX]; - int new_evdev_cnt = 0; - memset(evdevs, 0, sizeof(evdevs)); + struct pollfd pfds[EVDEV_CNT_MAX]; + int idx[EVDEV_CNT_MAX]; + int cnt = 0; + bool changed = false; - /* Open all event devices */ - char evdev_name[EVDEV_NAME_SIZE_MAX] = {0}; + char evdev_name[EVDEV_NAME_SIZE_MAX]; for (int i = 0; i < EVDEV_CNT_MAX; i++) { - /* Check if the file exists */ - snprintf(evdev_name, EVDEV_NAME_SIZE_MAX, "/dev/input/event%d", i); + snprintf(evdev_name, sizeof(evdev_name), "/dev/input/event%d", i); if (access(evdev_name, F_OK) != 0) continue; - /* Match device with the old device list */ - bool opened = false; - for (size_t j = 0; j < tm->evdev_cnt; j++) { - /* Copy the fd if the device is already on the list */ - if (tm->evdevs[j].idx == i) { - evdevs[new_evdev_cnt].idx = tm->evdevs[j].idx; - evdevs[new_evdev_cnt].fd = tm->evdevs[j].fd; - tm->evdevs[j].fd = -1; - new_evdev_cnt++; - opened = true; + /* Keep the descriptor of a device already open */ + int fd = -1; + for (int j = 0; j < tm->evdev_cnt; j++) { + if (tm->evdev_idx[j] == i) { + fd = tm->pfds[1 + j].fd; + tm->pfds[1 + j].fd = -1; break; } } - - /* Open the file if it is not on the list */ - int fd = open(evdev_name, O_RDWR | O_NONBLOCK); - if (fd >= 0 && !opened) { - /* Query absolute axis info for newly opened devices */ + if (fd < 0) { + fd = open(evdev_name, O_RDWR | O_NONBLOCK); + if (fd < 0) + continue; twin_linux_input_query_abs(fd, tm); - evdevs[new_evdev_cnt].idx = i; - evdevs[new_evdev_cnt].fd = fd; - new_evdev_cnt++; + changed = true; } + pfds[cnt] = (struct pollfd) {.fd = fd, .events = POLLIN}; + idx[cnt++] = i; } - /* Close disconnected devices */ - bool device_list_changed = false; - for (size_t i = 0; i < tm->evdev_cnt; i++) { - if (tm->evdevs[i].fd > 0) { - close(tm->evdevs[i].fd); - device_list_changed = true; + /* Whatever is left was disconnected */ + for (int j = 0; j < tm->evdev_cnt; j++) { + if (tm->pfds[1 + j].fd >= 0) { + close(tm->pfds[1 + j].fd); + changed = true; } } - /* Check if new devices were added */ - if (new_evdev_cnt != tm->evdev_cnt) - device_list_changed = true; - - /* Overwrite the evdev list */ - memcpy(tm->evdevs, evdevs, sizeof(tm->evdevs)); - tm->evdev_cnt = new_evdev_cnt; + memcpy(&tm->pfds[1], pfds, sizeof(pfds[0]) * cnt); + memcpy(tm->evdev_idx, idx, sizeof(idx[0]) * cnt); + tm->evdev_cnt = cnt; #if TWIN_INPUT_SMOOTH_WEIGHT > 0 - /* Reset smoothing state on device reconnection to prevent coordinate jumps - */ - if (device_list_changed && tm->smooth_initialized) + /* Reset smoothing on reconnection to prevent coordinate jumps */ + if (changed) tm->smooth_initialized = false; +#else + (void) changed; #endif - - /* Initialize evdev poll file descriptors */ - for (size_t i = tm->udev_cnt; i < tm->evdev_cnt + tm->udev_cnt; i++) { - pfds[i].fd = tm->evdevs[i - 1].fd; - pfds[i].events = POLLIN; - } } -static void *twin_linux_evdev_thread(void *arg) +void twin_linux_input_poll(twin_linux_input_t *tm, int timeout_ms) { - twin_linux_input_t *tm = arg; - - struct udev *udev = NULL; - struct udev_monitor *mon = NULL; - - /* Open Linux udev (user space device manager) */ - int udev_fd = twin_linux_udev_init(&udev, &mon); - if (udev_fd >= 0) - tm->udev_cnt = 1; - - /* Place the udev fd into the poll fds */ - struct pollfd pfds[UDEV_EVDEV_FD_CNT]; - pfds[0].fd = udev_fd; - pfds[0].events = POLLIN; - - /* Open event devices */ - twin_linux_edev_open(pfds, tm); - - /* Accessing to input devices is impossible, terminate the thread */ - if (tm->evdev_cnt == 0 && tm->udev_cnt == 0) { - log_error("Failed to open udev and evdev"); - pthread_exit(NULL); + /* The VT switch signals stay blocked outside this wait (see + * twin_vt_setup_signal_handler()): one sent just before it still ends + * it, with no window where the signal is handled and then slept on */ + sigset_t during; + sigprocmask(SIG_BLOCK, NULL, &during); + sigdelset(&during, SIGUSR1); + sigdelset(&during, SIGUSR2); + struct timespec ts = {timeout_ms / 1000, (timeout_ms % 1000) * 1000000L}; + if (ppoll(tm->pfds, 1 + tm->evdev_cnt, timeout_ms < 0 ? NULL : &ts, + &during) <= 0) + return; /* timed out, or a signal (a VT switch) woke us */ + + bool rescan = (tm->pfds[0].revents & POLLIN) && tm->mon && + twin_linux_udev_update(tm->mon); + if (tm->pfds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) { + /* A broken monitor would wake every poll: carry on without + * hotplug */ + log_warn("udev monitor failed; input hotplug disabled"); + udev_monitor_unref(tm->mon); + udev_unref(tm->udev); + tm->mon = NULL; + tm->udev = NULL; + tm->pfds[0].fd = -1; } - /* Event polling */ - struct input_event ev; - while (1) { - /* Wait until any event is available */ - if (poll(pfds, tm->evdev_cnt + tm->udev_cnt, -1) <= 0) + struct input_event ev[64]; + for (int i = 0; i < tm->evdev_cnt; i++) { + short revents = tm->pfds[1 + i].revents; + if (revents & (POLLERR | POLLHUP | POLLNVAL)) + rescan = true; + if (!(revents & POLLIN)) continue; - - /* Iterate through all file descriptors */ - for (size_t i = 0; i < tm->evdev_cnt + tm->udev_cnt; i++) { - if (i < tm->udev_cnt) { - /* Check udev event */ - if (twin_linux_udev_update(mon)) { - /* Re-open event devices */ - twin_linux_edev_open(pfds, tm); - break; - } - continue; - } - /* Try reading events */ - ssize_t n = read(pfds[i].fd, &ev, sizeof(ev)); - if (n == sizeof(ev)) { - /* Handle events */ - twin_linux_input_events(&ev, tm); - } - } + ssize_t n; + while ((n = read(tm->pfds[1 + i].fd, ev, sizeof(ev))) > 0) + for (size_t k = 0; k < (size_t) n / sizeof(ev[0]); k++) + twin_linux_input_events(&ev[k], tm); } - /* Clean up */ - udev_monitor_unref(mon); - udev_unref(udev); - - return NULL; + if (rescan) + twin_linux_edev_open(tm); } -void *twin_linux_input_create(twin_screen_t *screen) +twin_linux_input_t *twin_linux_input_create(twin_screen_t *screen) { - /* Create object for handling Linux input system */ - twin_linux_input_t *tm = calloc(1, sizeof(twin_linux_input_t)); + twin_linux_input_t *tm = calloc(1, sizeof(*tm)); if (!tm) return NULL; @@ -469,18 +417,36 @@ void *twin_linux_input_create(twin_screen_t *screen) tm->smooth_initialized = true; #endif - /* Start event handling thread */ - if (pthread_create(&tm->evdev_thread, NULL, twin_linux_evdev_thread, tm)) { - log_error("Failed to create evdev thread"); - return NULL; + /* Hotplug notifications, when udev is there */ + int udev_fd = twin_linux_udev_init(&tm->udev, &tm->mon); + if (udev_fd < 0) { + /* A monitor without a descriptor is no use; earlier failures + * already released what they made */ + if (tm->mon) { + udev_monitor_unref(tm->mon); + udev_unref(tm->udev); + } + tm->udev = NULL; + tm->mon = NULL; } + tm->pfds[0] = (struct pollfd) {.fd = udev_fd, .events = POLLIN}; + twin_linux_edev_open(tm); + /* The display stays usable without input: poll then only sleeps */ + if (tm->evdev_cnt == 0 && udev_fd < 0) + log_warn("No input devices and no udev: running without input"); return tm; } -void twin_linux_input_destroy(void *_tm) +void twin_linux_input_destroy(twin_linux_input_t *tm) { - twin_linux_input_t *tm = _tm; - close(tm->fd); + if (!tm) + return; + for (int i = 0; i < tm->evdev_cnt; i++) + close(tm->pfds[1 + i].fd); + if (tm->mon) + udev_monitor_unref(tm->mon); + if (tm->udev) + udev_unref(tm->udev); free(tm); } diff --git a/backend/linux_input.h b/backend/linux_input.h index 93480a47..935cab98 100644 --- a/backend/linux_input.h +++ b/backend/linux_input.h @@ -1,14 +1,25 @@ /* * Twin - A Tiny Window System - * Copyright (c) 2024 National Cheng Kung University, Taiwan + * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan * All rights reserved. */ #ifndef _LINUX_INPUT_H__ #define _LINUX_INPUT_H__ -void *twin_linux_input_create(twin_screen_t *screen); +#include -void twin_linux_input_destroy(void *tm); +/* Mouse and touch input from evdev, with udev hotplug. Everything runs on + * the thread that calls twin_linux_input_poll(), the one that runs the + * screen: no locking, no extra thread. */ +typedef struct twin_linux_input twin_linux_input_t; + +twin_linux_input_t *twin_linux_input_create(twin_screen_t *screen); + +void twin_linux_input_destroy(twin_linux_input_t *tm); + +/* Wait up to @timeout_ms (-1: until input arrives) for input, then + * dispatch whatever arrived to the screen */ +void twin_linux_input_poll(twin_linux_input_t *tm, int timeout_ms); #endif diff --git a/backend/linux_vt.h b/backend/linux_vt.h index 84a24ddd..3a1d0c77 100644 --- a/backend/linux_vt.h +++ b/backend/linux_vt.h @@ -13,15 +13,17 @@ #include #include #include +#include #include "twin_private.h" -#define VT_DEV_TTY_MAX 11 +#define VT_DEV_TTY_MAX 16 /* "/dev/tty" and any int */ #define SIG_SWITCH_FROM SIGUSR1 #define SIG_SWITCH_TO SIGUSR2 static volatile sig_atomic_t *vt_fd; -static volatile sig_atomic_t *is_vt_actived; +/* Set while the VT is switched away from us */ +static volatile sig_atomic_t *vt_away_flag; static inline int twin_vt_open(int vt_num) { @@ -50,7 +52,7 @@ static void twin_vt_process_switch_signal(int signum) /* Set the virtual terminal display in text mode */ ioctl(*vt_fd, KDSETMODE, KD_TEXT); - *is_vt_actived = true; + *vt_away_flag = true; } else if (signum == SIG_SWITCH_TO) { /* Switch complete */ ioctl(*vt_fd, VT_RELDISP, VT_ACKACQ); @@ -58,7 +60,7 @@ static void twin_vt_process_switch_signal(int signum) /* Restore the virtual terminal display in graphics mode */ ioctl(*vt_fd, KDSETMODE, KD_GRAPHICS); - *is_vt_actived = false; + *vt_away_flag = false; } } @@ -72,21 +74,44 @@ static inline void twin_vt_setup_signal_handler() /* Set the signal handler for returning to the previous TTY */ sigaction(SIG_SWITCH_TO, &tty_sig, NULL); + + /* Deliver them only while waiting for input: twin_linux_input_poll() + * unblocks them atomically, so the wait sees every switch */ + sigset_t sw; + sigemptyset(&sw); + sigaddset(&sw, SIG_SWITCH_FROM); + sigaddset(&sw, SIG_SWITCH_TO); + sigprocmask(SIG_BLOCK, &sw, NULL); +} + +/* Give the terminal back: the saved switching mode and text display */ +static inline void twin_vt_restore(int fd, const struct vt_mode *old_vtm) +{ + ioctl(fd, VT_SETMODE, old_vtm); + twin_vt_mode(fd, KD_TEXT); + sigset_t sw; + sigemptyset(&sw); + sigaddset(&sw, SIG_SWITCH_FROM); + sigaddset(&sw, SIG_SWITCH_TO); + sigprocmask(SIG_UNBLOCK, &sw, NULL); } static inline bool twin_vt_setup(int *fd_ptr, struct vt_mode *old_vtm, - bool *vt_active) + volatile sig_atomic_t *vt_away) { /* Open VT0 to inquire information */ - if ((*fd_ptr = twin_vt_open(0)) < -1) { + int vt0 = twin_vt_open(0); + if (vt0 < 0) { log_error("Failed to open VT0"); return false; } /* Inquire for current VT number */ struct vt_stat vt; - if (ioctl(*fd_ptr, VT_GETSTATE, &vt) == -1) { + int got = ioctl(vt0, VT_GETSTATE, &vt); + close(vt0); + if (got == -1) { log_error("Failed to get VT number"); return false; } @@ -94,26 +119,21 @@ static inline bool twin_vt_setup(int *fd_ptr, int vt_num = vt.v_active; /* Open the VT */ - if ((*fd_ptr = twin_vt_open(vt_num)) < -1) { + if ((*fd_ptr = twin_vt_open(vt_num)) < 0) return false; - } /* Attempt to activate the specified virtual terminal by its number */ - if (ioctl(*fd_ptr, VT_ACTIVATE, vt_num) < 0) { - log_error("Failed to activate VT %d", vt_num); - return false; - } - - /* Wait until the specified virtual terminal becomes the active VT */ - if (ioctl(*fd_ptr, VT_WAITACTIVE, vt_num) < 0) { + if (ioctl(*fd_ptr, VT_ACTIVATE, vt_num) < 0 || + /* and wait until it becomes the active VT */ + ioctl(*fd_ptr, VT_WAITACTIVE, vt_num) < 0) { log_error("Failed to activate VT %d", vt_num); - return false; + goto bail; } /* Save the virtual terminal mode */ - if (ioctl(*fd_ptr, VT_GETMODE, &old_vtm) < 0) { + if (ioctl(*fd_ptr, VT_GETMODE, old_vtm) < 0) { log_error("Failed to get VT mode"); - return false; + goto bail; } /* Set the virtual terminal mode */ @@ -122,19 +142,24 @@ static inline bool twin_vt_setup(int *fd_ptr, if (ioctl(*fd_ptr, VT_SETMODE, &vtm) < 0) { log_error("Failed to set VT mode"); - return false; + goto bail; } /* Set the virtual terminal display mode to graphics mode */ if (twin_vt_mode(*fd_ptr, KD_GRAPHICS) < 0) { log_error("Failed to set KD_GRAPHICS mode"); - twin_vt_mode(*fd_ptr, KD_TEXT); - return false; + twin_vt_restore(*fd_ptr, old_vtm); + goto bail; } vt_fd = fd_ptr; - is_vt_actived = vt_active; + vt_away_flag = vt_away; return true; + +bail: + close(*fd_ptr); + *fd_ptr = -1; + return false; } #endif diff --git a/backend/sdl.c b/backend/sdl.c index 04713fa1..2f4bac06 100644 --- a/backend/sdl.c +++ b/backend/sdl.c @@ -16,8 +16,9 @@ typedef struct { int *pixels; SDL_Renderer *render; SDL_Texture *texture; - twin_coord_t width, height; - int image_y; + /* Union of the rectangles scanned out since the last present */ + twin_coord_t dirty_left, dirty_top, dirty_right, dirty_bottom; + bool dirty; } twin_sdl_t; #define SCREEN(x) ((twin_context_t *) x)->screen @@ -30,9 +31,22 @@ static void _twin_sdl_put_begin(twin_coord_t left, void *closure) { twin_sdl_t *tx = PRIV(closure); - tx->width = right - left; - tx->height = bottom - top; - tx->image_y = top; + if (!tx->dirty) { + tx->dirty_left = left; + tx->dirty_top = top; + tx->dirty_right = right; + tx->dirty_bottom = bottom; + tx->dirty = true; + return; + } + if (left < tx->dirty_left) + tx->dirty_left = left; + if (top < tx->dirty_top) + tx->dirty_top = top; + if (right > tx->dirty_right) + tx->dirty_right = right; + if (bottom > tx->dirty_bottom) + tx->dirty_bottom = bottom; } static void _twin_sdl_put_span(twin_coord_t left, @@ -44,16 +58,23 @@ static void _twin_sdl_put_span(twin_coord_t left, twin_screen_t *screen = SCREEN(closure); twin_sdl_t *tx = PRIV(closure); - for (twin_coord_t ix = left, iy = top; ix < right; ix++) { - twin_argb32_t pixel = *pixels++; - tx->pixels[iy * screen->width + ix] = pixel; - } - if ((top + 1 - tx->image_y) == tx->height) { - SDL_UpdateTexture(tx->texture, NULL, tx->pixels, - screen->width * sizeof(*pixels)); - SDL_RenderCopy(tx->render, tx->texture, NULL, NULL); - SDL_RenderPresent(tx->render); - } + memcpy(&tx->pixels[top * screen->width + left], pixels, + (size_t) (right - left) * sizeof(*pixels)); +} + +/* Upload only what changed, then present once per screen update */ +static void _twin_sdl_present(twin_screen_t *screen, twin_sdl_t *tx) +{ + if (!tx->dirty) + return; + SDL_Rect r = {tx->dirty_left, tx->dirty_top, + tx->dirty_right - tx->dirty_left, + tx->dirty_bottom - tx->dirty_top}; + SDL_UpdateTexture(tx->texture, &r, &tx->pixels[r.y * screen->width + r.x], + screen->width * sizeof(*tx->pixels)); + SDL_RenderCopy(tx->render, tx->texture, NULL, NULL); + SDL_RenderPresent(tx->render); + tx->dirty = false; } static void _twin_sdl_destroy(twin_screen_t *screen maybe_unused, @@ -75,22 +96,33 @@ static void _twin_sdl_destroy(twin_screen_t *screen maybe_unused, SDL_Quit(); } -static void twin_sdl_damage(twin_screen_t *screen, twin_sdl_t *tx) -{ - int width, height; - SDL_GetWindowSize(tx->win, &width, &height); - twin_screen_damage(screen, 0, 0, width, height); -} - static bool twin_sdl_work(void *closure) { twin_screen_t *screen = SCREEN(closure); - if (twin_screen_damaged(screen)) + if (twin_screen_damaged(screen)) { twin_screen_update(screen); + _twin_sdl_present(screen, PRIV(closure)); + } return true; } +/* + * Window scale: MADO_SCALE=n, or 2x for phone-sized screens so a 240x320 + * display is readable on a desktop monitor. The renderer's logical size + * maps mouse coordinates back to screen pixels. + */ +static int _twin_sdl_scale(int width, int height) +{ + const char *env = getenv("MADO_SCALE"); + if (env) { + int s = atoi(env); + if (s >= 1 && s <= 8) + return s; + } + return (width <= 320 && height <= 480) ? 2 : 1; +} + twin_context_t *twin_sdl_init(int width, int height) { twin_context_t *ctx = calloc(1, sizeof(twin_context_t)); @@ -110,13 +142,18 @@ twin_context_t *twin_sdl_init(int width, int height) twin_sdl_t *tx = ctx->priv; static char *title = "twin-sdl"; + int scale = _twin_sdl_scale(width, height); tx->win = SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, width, height, - SDL_WINDOW_SHOWN); + SDL_WINDOWPOS_UNDEFINED, width * scale, + height * scale, SDL_WINDOW_SHOWN); if (!tx->win) { log_error("%s", SDL_GetError()); goto bail_sdl; } +#if defined(CONFIG_CURSOR) + /* The screen draws its own arrow: one pointer, not two */ + SDL_ShowCursor(SDL_DISABLE); +#endif tx->pixels = malloc(width * height * sizeof(*tx->pixels)); if (!tx->pixels) { @@ -125,11 +162,16 @@ twin_context_t *twin_sdl_init(int width, int height) } memset(tx->pixels, 255, width * height * sizeof(*tx->pixels)); + /* Mado composites in software anyway: any renderer that can present a + * texture will do, so fall back when there is no GPU. */ tx->render = SDL_CreateRenderer(tx->win, -1, SDL_RENDERER_ACCELERATED); + if (!tx->render) + tx->render = SDL_CreateRenderer(tx->win, -1, SDL_RENDERER_SOFTWARE); if (!tx->render) { log_error("%s", SDL_GetError()); goto bail_pixels; } + SDL_RenderSetLogicalSize(tx->render, width, height); SDL_SetRenderDrawColor(tx->render, 255, 255, 255, 255); SDL_RenderClear(tx->render); @@ -167,11 +209,12 @@ twin_context_t *twin_sdl_init(int width, int height) return NULL; } -static void twin_sdl_configure(twin_context_t *ctx) +/* Deliver a character to the focused window */ +static void _twin_sdl_ucs4(twin_screen_t *screen, twin_ucs4_t ucs4) { - int width, height; - SDL_GetWindowSize(PRIV(ctx)->win, &width, &height); - twin_screen_resize(ctx->screen, width, height); + twin_event_t tev = {.kind = TwinEventUcs4}; + tev.u.ucs4.ucs4 = ucs4; + twin_screen_dispatch(screen, &tev); } static bool twin_sdl_poll(twin_context_t *ctx) @@ -179,16 +222,21 @@ static bool twin_sdl_poll(twin_context_t *ctx) twin_screen_t *screen = SCREEN(ctx); twin_sdl_t *tx = PRIV(ctx); + /* Sleep until input arrives or the next timeout is due instead of + * spinning, waking at least every 100 ms; with NULL the event that + * wakes us stays queued for the loop below */ + twin_time_t wait = _twin_idle_timeout(screen, 100); + if (wait > 0) + SDL_WaitEventTimeout(NULL, wait); + SDL_Event ev; - bool has_event = false; while (SDL_PollEvent(&ev)) { - has_event = true; twin_event_t tev; switch (ev.type) { case SDL_WINDOWEVENT: if (ev.window.event == SDL_WINDOWEVENT_EXPOSED || ev.window.event == SDL_WINDOWEVENT_SHOWN) { - twin_sdl_damage(screen, tx); + twin_screen_damage(screen, 0, 0, screen->width, screen->height); } break; case SDL_QUIT: @@ -206,11 +254,45 @@ static bool twin_sdl_poll(twin_context_t *ctx) break; case SDL_KEYDOWN: case SDL_KEYUP: + /* Alt+Tab cycles the windows instead of reaching the app */ + if (ev.key.keysym.sym == SDLK_TAB && + (ev.key.keysym.mod & KMOD_ALT)) { + if (ev.type == SDL_KEYDOWN) + twin_screen_focus_next(screen); + break; + } tev.u.key.key = ev.key.keysym.sym; tev.kind = ((ev.key.type == SDL_KEYDOWN) ? TwinEventKeyDown : TwinEventKeyUp); twin_screen_dispatch(screen, &tev); + /* Editing keys produce no SDL text input; send them as UCS-4 */ + if (ev.type == SDL_KEYDOWN) { + switch (ev.key.keysym.sym) { + case SDLK_RETURN: + case SDLK_KP_ENTER: + _twin_sdl_ucs4(screen, '\r'); + break; + case SDLK_BACKSPACE: + _twin_sdl_ucs4(screen, 8); + break; + case SDLK_ESCAPE: + _twin_sdl_ucs4(screen, 27); + break; + default: + break; + } + } + break; + case SDL_TEXTINPUT: { + twin_ucs4_t ucs4; + const char *p = ev.text.text; + int len; + while ((len = _twin_utf8_to_ucs4(p, &ucs4)) > 0) { + _twin_sdl_ucs4(screen, ucs4); + p += len; + } break; + } case SDL_MOUSEMOTION: tev.u.pointer.screen_x = ev.motion.x; tev.u.pointer.screen_y = ev.motion.y; @@ -221,12 +303,6 @@ static bool twin_sdl_poll(twin_context_t *ctx) } } - /* Yield CPU when idle to avoid busy-waiting. - * Skip delay if events were processed or screen needs update. - */ - if (!has_event && !twin_screen_damaged(screen)) - SDL_Delay(1); /* 1ms sleep reduces CPU usage when idle */ - return true; } @@ -252,24 +328,10 @@ static void twin_sdl_exit(twin_context_t *ctx) free(ctx); } -static void twin_sdl_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - /* Initialize immediately and enter standard dispatch loop */ - if (init_callback) - init_callback(ctx); - - /* Use twin_dispatch_once() to ensure work queue and timeouts run */ - while (twin_dispatch_once(ctx)) - ; -} - /* Register the SDL backend */ const twin_backend_t g_twin_backend = { .init = twin_sdl_init, - .configure = twin_sdl_configure, .poll = twin_sdl_poll, - .start = twin_sdl_start, .exit = twin_sdl_exit, }; diff --git a/backend/vnc.c b/backend/vnc.c index 123aa3c3..ee54614d 100644 --- a/backend/vnc.c +++ b/backend/vnc.c @@ -55,12 +55,10 @@ static void _twin_vnc_put_begin(twin_coord_t left, twin_coord_t bottom, void *closure) { - (void) left; - (void) top; - (void) right; - (void) bottom; + /* Only the rectangle being scanned out goes to the clients */ twin_vnc_t *tx = PRIV(closure); - pixman_region_init_rect(&tx->damage_region, 0, 0, tx->width, tx->height); + pixman_region_union_rect(&tx->damage_region, &tx->damage_region, left, top, + right - left, bottom - top); } static void _twin_vnc_put_span(twin_coord_t left, @@ -74,14 +72,6 @@ static void _twin_vnc_put_span(twin_coord_t left, size_t span_width = right - left; memcpy(fb_pixels, pixels, span_width * sizeof(*fb_pixels)); - pixman_region_union_rect(&tx->damage_region, &tx->damage_region, left, top, - span_width, 1); -} - -static void twin_vnc_get_screen_size(twin_vnc_t *tx, int *width, int *height) -{ - *width = nvnc_fb_get_width(tx->current_fb); - *height = nvnc_fb_get_height(tx->current_fb); } static bool _twin_vnc_work(void *closure) @@ -106,7 +96,11 @@ static void _twin_vnc_client_cleanup(struct nvnc_client *client) static void _twin_vnc_new_client(struct nvnc_client *client) { - twin_peer_t *peer = malloc(sizeof(twin_peer_t)); + twin_peer_t *peer = calloc(1, sizeof(twin_peer_t)); + if (!peer) { + nvnc_client_close(client); + return; + } nvnc_set_userdata(client, peer, NULL); nvnc_set_client_cleanup_fn(client, _twin_vnc_client_cleanup); } @@ -117,32 +111,29 @@ static void _twin_vnc_pointer_event(struct nvnc_client *client, enum nvnc_button_mask button) { twin_peer_t *peer = nvnc_get_userdata(client); - twin_event_t tev; - if ((button & NVNC_BUTTON_LEFT) && - !(peer->prev_button & NVNC_BUTTON_LEFT)) { - tev.u.pointer.screen_x = x; - tev.u.pointer.screen_y = y; - tev.kind = TwinEventButtonDown; - tev.u.pointer.button = 1; - } else if (!(button & NVNC_BUTTON_LEFT) && - (peer->prev_button & NVNC_BUTTON_LEFT)) { - tev.u.pointer.screen_x = x; - tev.u.pointer.screen_y = y; - tev.kind = TwinEventButtonUp; - tev.u.pointer.button = 1; - } - if ((peer->px != x || peer->py != y)) { + struct nvnc *server = nvnc_client_get_server(client); + twin_vnc_t *tx = nvnc_get_userdata(server); + bool down = button & NVNC_BUTTON_LEFT; + bool was_down = peer->prev_button & NVNC_BUTTON_LEFT; + twin_event_t tev = {0}; + tev.u.pointer.screen_x = x; + tev.u.pointer.screen_y = y; + + /* Motion first, then the button change, so a press arriving with a + * move lands where the pointer ended up */ + if (peer->px != x || peer->py != y) { peer->px = x; peer->py = y; - tev.u.pointer.screen_x = x; - tev.u.pointer.screen_y = y; - tev.u.pointer.button = 0; tev.kind = TwinEventMotion; + tev.u.pointer.button = was_down; + twin_screen_dispatch(tx->screen, &tev); + } + if (down != was_down) { + tev.kind = down ? TwinEventButtonDown : TwinEventButtonUp; + tev.u.pointer.button = 1; + twin_screen_dispatch(tx->screen, &tev); } peer->prev_button = button; - struct nvnc *server = nvnc_client_get_server(client); - twin_vnc_t *tx = nvnc_get_userdata(server); - twin_screen_dispatch(tx->screen, &tev); } static struct nvnc_fb *_twin_vnc_create_cursor() @@ -236,6 +227,7 @@ twin_context_t *twin_vnc_init(int width, int height) goto bail_framebuffer; } + pixman_region_init(&tx->damage_region); twin_set_work(_twin_vnc_work, TWIN_WORK_REDISPLAY, ctx); tx->screen = ctx->screen; @@ -260,25 +252,19 @@ twin_context_t *twin_vnc_init(int width, int height) static bool twin_vnc_poll(twin_context_t *ctx) { twin_vnc_t *tx = PRIV(ctx); - aml_poll(tx->aml, 0); + /* Sleep until a client talks or there is something to show */ + aml_poll(tx->aml, (int) _twin_idle_timeout(SCREEN(ctx), -1)); aml_dispatch(tx->aml); return true; } -static void twin_vnc_configure(twin_context_t *ctx) -{ - int width, height; - twin_vnc_t *tx = ctx->priv; - twin_vnc_get_screen_size(tx, &width, &height); - twin_screen_resize(ctx->screen, width, height); -} - static void twin_vnc_exit(twin_context_t *ctx) { if (!ctx) return; twin_vnc_t *tx = PRIV(ctx); + pixman_region_fini(&tx->damage_region); nvnc_fb_unref(tx->current_fb); nvnc_display_unref(tx->display); nvnc_close(tx->server); @@ -289,22 +275,8 @@ static void twin_vnc_exit(twin_context_t *ctx) free(ctx); } -/* Start function for VNC backend */ -static void twin_vnc_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) -{ - if (init_callback) - init_callback(ctx); - - /* Use standard dispatcher to ensure work queue and timeouts run */ - while (twin_dispatch_once(ctx)) - ; -} - const twin_backend_t g_twin_backend = { .init = twin_vnc_init, .poll = twin_vnc_poll, - .configure = twin_vnc_configure, - .start = twin_vnc_start, .exit = twin_vnc_exit, }; diff --git a/backend/wasm.c b/backend/wasm.c index a47cfea8..804f8c40 100644 --- a/backend/wasm.c +++ b/backend/wasm.c @@ -134,7 +134,7 @@ static twin_context_t *twin_wasm_init(int width, int height) } /* Register work callback for rendering */ - twin_set_work(_twin_wasm_work, 0, ctx); + twin_set_work(_twin_wasm_work, TWIN_WORK_REDISPLAY, ctx); /* Store global context before JavaScript initialization * (required by accessor functions) @@ -151,15 +151,6 @@ static twin_context_t *twin_wasm_init(int width, int height) return ctx; } -static void twin_wasm_configure(twin_context_t *ctx) -{ - /* WebAssembly configuration - * For now, we use fixed canvas size from initialization. - * TODO: Support dynamic canvas resizing via JS callback. - */ - (void) ctx; -} - /* Event polling - called by main loop * Events are injected via exported C functions from JavaScript */ @@ -170,7 +161,7 @@ static bool twin_wasm_poll(twin_context_t *ctx) } /* Emscripten main loop state */ -static void (*g_wasm_init_callback)(twin_context_t *) = NULL; +static void (*g_wasm_init_callback)(twin_screen_t *) = NULL; static bool g_wasm_initialized = false; /* Main loop callback for Emscripten */ @@ -180,7 +171,7 @@ static void twin_wasm_loop(void *arg) /* Perform one-time initialization on first iteration */ if (!g_wasm_initialized && g_wasm_init_callback) { - g_wasm_init_callback(ctx); + g_wasm_init_callback(ctx->screen); g_wasm_initialized = true; } @@ -190,7 +181,7 @@ static void twin_wasm_loop(void *arg) /* Start the main loop */ static void twin_wasm_start(twin_context_t *ctx, - void (*init_callback)(twin_context_t *)) + void (*init_callback)(twin_screen_t *)) { g_wasm_init_callback = init_callback; g_wasm_initialized = false; @@ -331,7 +322,6 @@ void mado_wasm_shutdown(void) /* Register the WebAssembly backend */ const twin_backend_t g_twin_backend = { .init = twin_wasm_init, - .configure = twin_wasm_configure, .poll = twin_wasm_poll, .start = twin_wasm_start, .exit = twin_wasm_exit, diff --git a/configs/Kconfig b/configs/Kconfig index e44bbc9a..1a2e349b 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -180,6 +180,15 @@ config LOGGING_CALLBACK Allow custom callback functions for log handling. Enables application-specific log processing. +config OPTIMIZE_SIZE + bool "Optimize for size" + default n + help + Build with -Os instead of -O2 and without unwind tables, which a C + library does not need. On RV32IM this shrinks the library by about + a fifth; the rendering hot paths run somewhat slower. Leave it off + on hosts, where debuggers and Valgrind use the unwind tables. + config MEMORY_STATS bool "Enable memory usage tracking" default y @@ -225,30 +234,61 @@ config CURSOR Enable custom cursor rendering and manipulation. Allows application control over cursor appearance. -config WINDOW_MANAGER - bool "Enable window manager" - default y +menu "Window Management" + +choice + prompt "Window manager" + default WM_DESKTOP help - Enable the window management subsystem: title bars, - decorations, drag-move, click-to-raise, iconify/restore, - and resize handles. + Select the window manager built into the library. Every screen + runs it for its whole life: there is no switching at run time. - When disabled, windows are placed at (0,0) and clamped to - screen size. No decorations are drawn; the client area - covers the full pixmap. Pointer events route directly to - the window event handler without WM interpretation. The - demo launcher starts only the first enabled demo in this - mode to avoid inaccessible overlapping windows. +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. - Disabling saves ~3-4 KB .text on ARM Cortex-M class targets. + Costs ~8 KB .text on RV32IM (title bars add src/icon.c). -config DROP_SHADOW - bool "Render drop shadow for active window" +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 WINDOW_MANAGER + depends on WM_MOBILE help - Add drop shadow effect to active window. - Provides visual depth and window focus indication. + 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" @@ -276,20 +316,70 @@ config SHADOW_BLUR range 1 15 depends on DROP_SHADOW help - Shadow blur kernel size (radius). + Width in pixels of the shadow's soft edge. Default 12 approximates common CSS box-shadow softness. - Larger values create softer, more diffuse shadows. - Higher values increase rendering cost. + Larger values create softer, more diffuse shadows. The shadow + is computed while scanning out, so the radius does not change + its cost per pixel. -config SHADOW_FADE_TAIL - int "Shadow fade tail" - default 2 - range 0 3 - depends on DROP_SHADOW +endmenu + +menu "Rendering" + +config SCRATCH_SIZE + int "Rasterizer scratch arena (KiB)" + default 16 + range 0 1024 + help + Per-screen arena that holds the coverage mask and edges of each + filled path. A shape whose mask does not fit is rasterized in + bands of rows that fill half the arena, so a full-size mask never + comes from the heap; a smaller arena means more bands on large + shapes. Only when the arena cannot hold a band (0, or a shape + wider than the arena allows) does a small band come from the heap. + +config CACHE_SIZE + int "Coverage cache budget (KiB)" + default 24 + range 0 4096 + help + Byte budget for cached anti-aliased coverage masks of glyphs and + small filled paths. Repainting a screen of text and rounded + shapes then composites cached masks instead of re-rasterizing. + 0 disables the cache. + +config CACHE_MAX_ENTRY + int "Largest cached mask (bytes)" + default 4096 + range 256 65536 + depends on CACHE_SIZE != 0 + help + Masks larger than this are rasterized every time, clipped to the + visible region, so one big fill cannot flush the working set. + +config WIDGETS + bool "Box layout widget toolkit" + 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. + +config FONT_MONTSERRAT + bool "Montserrat Medium font" + default y + help + TrueType outlines of Montserrat Medium (SIL OFL 1.1), ASCII plus a + few typographic marks. About 7 KB of read-only data. + +config FONT_ICONS + bool "UI symbol font" + default y help - Additional zero-alpha pixels appended to the shadow mask. - Default adds ~2 px of clear space to match CSS feathering. - Higher values slightly enlarge the off-screen buffer usage. + Fifty-one Font Awesome 5 Free Solid symbols (CC BY 4.0) for status + bars, navigation and app icons, as TWIN_SYM_* UTF-8 strings. + +endmenu endmenu @@ -347,20 +437,41 @@ config DEMO_APPLICATIONS Build demonstration applications showcasing Mado features. Provides example code and testing targets. +config DEMO_CELLPHONE + bool "Build cellphone demo" + default y + depends on DEMO_APPLICATIONS && FONT_MONTSERRAT && FONT_ICONS + depends on WM_MOBILE + help + Phone user interface: lock screen, home screen, dialer, contacts, + messages, calculator, music, photos, camera, settings, call log and + games on the mobile window manager, which it needs. The other demos + selected below get launcher icons too and run as stacked apps; Back + closes their windows one at a time. + +config DEMO_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" default y - depends on DEMO_APPLICATIONS && WINDOW_MANAGER + depends on DEMO_APPLICATIONS && WIDGETS help Comprehensive demo with multiple windows and widgets. - Demonstrates window management and event handling. - Requires window manager for staggered placement and - overlapping window interaction. + Demonstrates window management and event handling: the desktop + policy staggers its windows, the mobile one stacks them and Back + closes one at a time. config DEMO_CLOCK bool "Build clock demo" default y - depends on DEMO_APPLICATIONS + depends on DEMO_APPLICATIONS && WIDGETS help Analog clock demonstration with real-time updates. Showcases CORDIC trigonometry functions. @@ -369,7 +480,7 @@ config DEMO_CLOCK config DEMO_CALCULATOR bool "Build calculator demo" default y - depends on DEMO_APPLICATIONS + depends on DEMO_APPLICATIONS && WIDGETS help Interactive calculator application. Demonstrates button widgets and event handling. @@ -378,7 +489,7 @@ config DEMO_CALCULATOR config DEMO_SPLINE bool "Build spline demo" default y - depends on DEMO_APPLICATIONS + depends on DEMO_APPLICATIONS && WIDGETS help Bezier spline curve demonstration. Shows advanced path rendering capabilities. @@ -387,7 +498,7 @@ config DEMO_SPLINE config DEMO_ANIMATION bool "Build animation demo" default y - depends on DEMO_APPLICATIONS + depends on DEMO_APPLICATIONS && WIDGETS help Animation demonstration with moving objects. Shows frame-based animation techniques. @@ -397,7 +508,7 @@ config DEMO_IMAGE bool "Build scalable image demo" select LOADER_TVG default y - depends on DEMO_APPLICATIONS + depends on DEMO_APPLICATIONS && WIDGETS help Scalable vector graphics demonstration. Shows TinyVG format rendering capabilities. diff --git a/docs/backends.md b/docs/backends.md index bfc663eb..2b31e165 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -32,7 +32,12 @@ Run: ./demo-sdl ``` -Once the window appears, you can move windows and interact with widgets. +Once the window appears, move windows and interact with widgets (the default +desktop build), or use the phone in a `WM_MOBILE` build. Screens up to +320x480 open at twice their size so a phone display is readable on a desktop +monitor; `MADO_SCALE=n` picks another integer factor from 1 through 8. +When nothing is damaged the backend waits for input or the next timeout, +waking at least every 100 ms, and presents once per screen update. Use Cases: - Desktop applications @@ -360,6 +365,7 @@ The `headless-ctl` tool provides command-line access to the headless backend: - Supports PNG format with minimal dependencies - No external image libraries required - `shutdown`: Gracefully shutdown the backend +- `type TEXT`: Inject TEXT as character (UCS-4) events to the focused window - `mouse TYPE X Y [BUTTON]`: Inject mouse events - TYPE: move, down, up - X, Y: Screen coordinates (validated against screen size) @@ -393,6 +399,9 @@ Screenshot saved to screenshot.png ./headless-ctl mouse down 100 200 1 ./headless-ctl mouse up 100 200 1 +# Type text into the focused window (delivered as UCS-4 key events) +./headless-ctl type "12+30=" + # Monitor backend activity in real-time $ ./headless-ctl monitor Monitoring backend activity (Ctrl+C to stop)... diff --git a/docs/phone.md b/docs/phone.md new file mode 100644 index 00000000..e45aff85 --- /dev/null +++ b/docs/phone.md @@ -0,0 +1,404 @@ +# Mobile and Desktop Scenes + +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 +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. + +## Window management is a policy + +A window is a pixmap plus client callbacks. Everything else (where the +window goes, whether it has a title bar, which window gets the pointer, +what happens when it closes) belongs to a *window manager policy* chosen +when the library is built: + +| Policy | Behavior | Kconfig | +|--------|----------|---------| +| none (minimal) | No decorations; application windows cover the screen; the newest window, or the one last pressed, has the keyboard, and hiding it hands the keyboard to the window below. | `WM_NONE` | +| desktop (classic) | Overlapping decorated windows, click-to-raise, drag-to-move (the title bar always stays reachable), double-click the title to maximize, drop on a screen edge to snap, drag the bottom-right client corner to resize, iconify, close, shadow under the active window. New windows cascade off occupied spots. | `WM_DESKTOP` | +| mobile (embedded) | Application windows stack over the work area between optional system bars; only the top one is visible; animated push/pop/dismiss, an edge-swipe back gesture and Escape for back. | `WM_MOBILE` | + +The Kconfig "Window manager" choice builds exactly one of them, and every +screen runs it for its whole life: there is no switching and no run-time +check of which policy is active. Keeping one policy per build keeps the +others' code and state out of targets that never use them, and a product +ships as a phone, a desktop or a kiosk, not as all three. Code and +fields only one policy needs are compiled out of the others: system bars +and the keyboard inset exist only in mobile builds, title bars and the +window title only in desktop builds, and drop shadows only with +`DROP_SHADOW`. + +```c +twin_screen_set_bars(screen, status, nav); /* mobile system bars */ +twin_window_close(window); /* WM-aware close */ +twin_window_back(window); /* in-app back, else close */ +twin_window_activate(window); /* raise and focus via the WM */ +twin_screen_focus_next(screen); /* cycle windows (Alt+Tab) */ +twin_screen_set_inset(screen, kbd_height); /* keep an on-screen keyboard clear */ +``` + +Applications describe intent through the window style rather than +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 +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 +compiled in only there. A desktop build opens the apps as overlapping +windows, the phone launcher lists them and the mobile policy stacks their +windows, and a `WM_NONE` build shows the first one full screen. +`test-demos` runs each app under the policy of the build. +`screen->windows` lists every window bottom to top and is the stacking +order for all policies. + +The policy interface (`src/twin_private.h`) is a set of `_twin_wm_*` +functions that the built policy defines and generic code calls directly, +with no function-pointer table: init/fini, layout, map/unmap, dispatch, +refresh (re-place the stack after a style, opacity, system-bar, inset or +screen-size change), and, for the policies that need them, decorate, hit +test and activate (desktop) or a pointer filter for gestures and the work +area (mobile). A hook a policy has no use for is an inline no-op, so the +call compiles away; outside the desktop, activation is the policy's map. +Policy state is allocated with the screen, so starting a policy cannot +fail. The work area counts only mapped bars, minus the inset set with +`twin_screen_set_inset()`. +`twin_screen_resize()` (a rotated panel, a resized host window) re-lays out +every window: phone apps and bars refit, desktop windows come back on +screen and maximized ones refit. + +The system bars are registered, not owned: a replaced bar is unmapped, a +destroyed bar unregisters itself, and resizing a bar re-lays out the +applications. + +Back is a request, not a close: the edge swipe and Escape send +`TwinEventBack`, so an app with its own history steps back inside its +window (the swipe then springs home); unhandled, the window closes. The +swipe only starts after the finger moves sideways past a few pixels, so a +tap or a vertical drag at the left edge still reaches the app. A press +that lands during a transition is dropped together with its release, and +a push moves the keyboard to the incoming app at once. + +Pointer grabs are explicit: the release goes to the window that took the +press, even when the pointer ends up over another one, and hover updates +re-check their targets after each Leave/Enter handler, so a window may +destroy itself from any of its own handlers. Generic code never asks +which policy is running. `src/wm.c` holds the screen-wide services; +`src/wm-none.c`, `src/wm-desktop.c` and `src/wm-mobile.c` are the +policies. A screen being destroyed first lets its policy go (`fini`) and +then tears the windows down without it, so the policy never sees half a +teardown. + +Policy state stays out of the compositor. Iconifying, for instance, is the +desktop policy limiting how many rows of the window's pixmap are shown +(`scan_height`): the compositor, damage and hit testing honour that limit +without knowing why it is set, and the hidden rows keep their pixels for +the restore. + +### Mobile transitions cost no repainting + +A push slides the new app in from the right while the previous one drifts +left by a third and dims; a pop reverses it; a full-screen window (the lock +screen) slides up when dismissed. None of this repaints an application: +the policy only moves retained pixmaps and changes their opacity, and the +compositor does the rest. When an application destroys its window, the +policy adopts the window's pixmap and animates that, so application state +is freed immediately and never touched by the animation. + +Every arrangement ends in `mobile_settle()`, which derives the visible set +from the window list alone. Transitions only perturb positions and opacity +in between, so interrupting one (a tap, a second push, a resize) is +always safe: finish, settle, start the next. + +### Memory: one surface regardless of stack depth + +Only the top application is visible under the mobile policy, so the +pixels of every application below it are released (eviction, Kconfig +`WM_MOBILE_EVICT`). The window keeps a storage-less stub pixmap whose +empty clip turns every drawing call into a no-op, accumulates damage, and +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. + +Heap measured by the `test-cellphone` budget gate: everything allocated +through `twin_malloc` except the bounded reuse pools (the 24 KiB coverage +cache and the path, mask and span buffers). The peak is the home figure +plus the allocator's growth while every app opens, so it includes the second +surface alive during transitions and decoded photos: + +| Surfaces | At home | Peak across all twelve apps | +|----------|---------|-----------------------------| +| ARGB32 | 330 KB | 724 KB | +| RGB565 | 176 KB | 446 KB | + +The trade-off is deliberate: a renderer that draws straight into partial +scan-out buffers needs less RAM, but must repaint every pixel of every +animation frame. Retaining the visible screen lets push, pop, swipe and +scroll run on memmove and compositing alone, which is what keeps them at +display rate on a CPU without graphics hardware. + +## Compositor + +The screen never holds a full framebuffer. Each update composes the +damaged region scanline by scanline from the window pixmaps into one span +buffer and hands it to the backend. + +- **Damage is a short list.** Up to eight rectangles per screen and four + per window. Two rectangles merge only when their union covers no more + pixels than the pair, so a clock tick in the status bar and a progress + bar in the app stay two small updates. Windows paint each damaged + rectangle under its own clip. +- **Occlusion culling.** Walking layers top-down, every opaque layer adds + its extent on the current row to a small set of covered intervals; + layers below are composited only where still visible and the background + only fills what nothing covers. Opaque regions come from RGB565 pixmaps + and windows that declare `twin_window_set_opaque()`. +- **Opacity.** The window manager can give a pixmap a constant alpha + applied at scan-out (fades, dimming during transitions). +- **Procedural shadows.** The window manager can attach a shadow + that is synthesized during scan-out from a separable smoothstep profile, + as a pseudo-layer directly under its pixmap. It costs four bytes per + pixmap: no enlarged pixmap, no blur pass, no temporary copy. The opaque + pixmap above culls its interior automatically. +- **Scrolling.** `twin_window_scroll()` moves retained pixels and damages + only the exposed band. Damage still pending inside the area moves with + the pixels, so stale pixels are repainted where they land. + +`twin_screen_stats()` counts frames, rectangles, pixels sent to the backend, +layer pixels composited, pixels skipped by occlusion and shadow pixels. +The counts are deterministic, which lets tests gate on work done. + +## Vector graphics and caching + +Anti-aliased rasterization (4x4 supersampled scan conversion) dominates the +cost of redrawing vector UI, and most of it repeats: a list, a keypad or a +home screen redraws the same few dozen shapes. + +The **coverage cache** (`src/cache.c`) keeps A8 coverage masks in one LRU +with a byte budget (`CACHE_SIZE`, KiB). Glyphs are keyed by font, size and +code point; filled paths by a hash of their device-space outline relative +to the pixel grid, so the same shape drawn at another integer offset hits. +Because the rasterizer's sample grid is pixel aligned, a cached mask is +bit-identical to a fresh one; the tests verify this. Masks larger than +`CACHE_MAX_ENTRY` bytes are never admitted, and rasterization of large +paths is limited to the visible part of the clip. + +**Text** (`twin_text_draw()`, `twin_text_width()`) composites cached glyph +masks with integer-snapped origins and 16.16 advances, and skips lines +outside the clip after measuring them. Fonts are selectable per call. Montserrat Medium and a 38-symbol +Font Awesome subset ship as TrueType outlines (`FONT_MONTSERRAT`, +`FONT_ICONS`, symbols as `TWIN_SYM_*` UTF-8 strings), generated by the +upgraded `tools/ttf/twin-ttf`. + +**Gradients** (`twin_paint_path_linear()`) reuse the cached mask; vertical +gradients cost one solid span per scanline. + +**Tweens** (`twin_tween_start()`) drive animation from elapsed time with +fixed-point easing curves, so a slow frame shortens an animation instead of +stretching it. `twin_now()` is the shared clock; tests replace it. + +Measured with `MADO_PERF_ONLY=vector ./mado-perf` on an x86-64 host with +the built-in renderer: + +| Operation | Plain | Optimized | Speed-up | +|-----------|-------|-----------|----------| +| 20 glyphs at 14 px | 74.8 µs uncached | 10.2 µs cached | 7.3x | +| 44x44 rounded rectangle | 18.5 µs uncached | 4.6 µs cached | 4.0x | +| 240x320 scan-out, app over app plus status bar | 146.6 µs | 45.4 µs culled | 3.2x | + +## API changes + +Added: window manager policies and system bars, `TwinWindowFullscreen`, +`TwinEventClose` and `twin_window_close()`, `twin_window_set_opaque()`, +`twin_window_scroll()`, pixmap opacity and shadows, compositor statistics, +`twin_text_*`, `twin_paint_path_linear()`, tweens, `twin_now()` and +`twin_toplevel_destroy()`. + +Removed or made internal: the compositor-level pixmap calls (show, hide, +move, opacity, shadow, update batching), the work queue, cursor setup +(`twin_create()` installs the arrow under `CONFIG_CURSOR`), +`twin_context_screen()` (`twin_run()` hands the callback the screen), +font measuring internals and the cache controls. Simplified: +`twin_toplevel_create()` (always an ARGB32 application window), +`twin_pixmap_from_file()` (always ARGB32), `twin_tvg_to_pixmap()` (one +call), `twin_label_set()` (text only), custom widgets (the handler gets +the custom widget; no lookup), and `twin_window_damage()`, which queues +the repaint itself. + +Removed or made internal, because nothing needed them or they encoded the +old fixed window manager: `twin_stack_blur()`, the pixmap-baked shadow +(`twin_shadow_border()`, `window->shadow_x/y`, the Gaussian LUT +generator), `twin_pixmap_transparent()`, `twin_pixmap_is_iconified()`, +`twin_screen_get_background()`, `twin_pixmap_create_budget()`, +`twin_custom_widget_base()`, the never-implemented +`twin_path_ucs4_stroke()`, joystick events, `CONFIG_WINDOW_MANAGER` and +`CONFIG_SHADOW_FADE_TAIL`. Nothing in the tree called `twin_sin()`, +`twin_tan()`, `twin_atan2()`, `twin_path_rmove()`, `twin_path_rdraw()`, +`twin_path_identity()`, the path font getters, `twin_screen_get_active()`, +`twin_widget_create()`, toolkit keyboard focus +(`twin_widget_set_focusable()`), `TwinStyleBoldOblique`, +`twin_event_queue_t` or 13 of the icon symbols, so they are gone (the icon +font shrank with them). Helpers only the library uses (matrix, path and +pixmap internals, `twin_icon_draw()`, the animation iterator) moved to +`src/twin_private.h`; an animated pixmap now owns its animation and frees +it in `twin_pixmap_destroy()`. The box/label/button toolkit is now +optional (`WIDGETS`); the phone does not use it. + +`include/twin.h` holds only the interface. Screens, windows, pixmaps, +fonts, animations, widgets and the context are opaque; their layouts live +in `src/twin_private.h`, and applications use accessors instead +(`twin_window_pixmap()`, `twin_window_client()`, `twin_window_set_callbacks()`, +`twin_pixmap_width()`, `twin_pixmap_set_transform()`, `twin_screen_width()`, +`twin_toplevel_box()`, `twin_button_widget()` and similar). The backend +interface (`twin_screen_create()`, `twin_screen_update()`, +`twin_screen_dispatch()` and the span callbacks) moved there too: only +backends, the tests and `mado-perf` drive a screen directly. Without a +window manager, the newest mapped window now takes the keyboard. + +Fixed along the way: TrueType glyphs were decoded with the wrong header +size and orientation, and the converter emitted cubic segments as +quadratics; RGB565 pixmaps were composited incorrectly (a macro evaluated +`*p++` several times); the closure tracker leaked an entry per distinct +closure until scheduling failed, and timeouts or work whose owner died were +retried forever; display-list insertion missed a back link; destroyed +windows could stay referenced as the active or pointer target; scaled +composites sampled the wrong source pixels when clipped; the widget toolkit +never freed widgets, custom-widget data or toplevels; the SDL backend +refused to start without an accelerated renderer and presented once per +damage rectangle; the headless backend recomposited on every damage call +and busy-waited; the fbdev and DRM backends no longer compiled with current +GCC; several left shifts of negative values; and `make check` ignored +header dependencies. + +## The phone + +`apps/cellphone/phone.h` is the whole contract for apps. A screen is a +*view*: one window plus a C struct that embeds `ui_view_t` first. Views +paint immediate-mode with the kit's painters (text, symbols, rounded +rectangles, gradients, list rows, buttons, sliders, switches, a soft +keyboard, text fields) and receive gesture events (press, drag with +velocity, release, tap, key). There is no per-widget object tree; state +is plain fields in the view struct, which is why a covered screen can be +evicted and repainted at any time. Timers and tweens created through the +view die with it. + +`shell.c` owns the themes (Sage, Dark, Ocean), wallpapers, clock, app +registry with squircle icon plates, the status and navigation bars, and +navigation (`phone_open()`, `phone_back()`, `phone_home()`), which is +nothing more than showing and closing views: the window manager owns the +stack, the transitions and the memory. Navigation acts on the topmost +application window whether or not it is a view, so a registry app opened +from the launcher goes back and home like any phone app. + +### Visual language: Sage + +The default theme, Sage, is a soft information-appliance style written +along the lines of the GNOME HIG: warm white pages, sage green grouping +and deep olive bars give a quiet reading surface, and rounded controls +with a few tactile launcher icons keep it friendly. It is a custom design +language, not Adwaita. + +- Three surfaces carry the structure, named in `phone_theme_t`: the page + and its content (`bg`, `card`), grouped areas a step darker (`group`: + section labels, the settings icon column, the navigation tray and the + launcher behind its tiles), and the deep bars (`statusbar`, `header`) + with `on_bar` text. Spacing, tone and thin separators divide content; + shadows only mark a control as raised. +- Colors keep one meaning: `primary` (olive) marks the selection and the + main action, `call_red` a destructive one. A state is never shown by + color alone: the current navigation tab is also the filled key, a + switch also moves its knob, a radio also fills its dot. +- Header bars place one start action (Contacts puts Add there), the title + centered, and an end action or short status text; header buttons have + no fill until pressed. The title moves to the start only when the end + text would collide with it. +- Buttons are soft rounded fills lit slightly from the top, with labels + that name the action. The navigation bar reaches the top-level pages + (Dialer, Home, Contacts), so its keys pair an icon with a label; + ordinary buttons carry one or the other. Above home, its center key + turns into Back. +- Settings apply immediately; there is no OK or Apply step. Rows use one + set of single-color symbols, while the launcher keeps small tactile + icons, so decoration and operation stay apart. +- Dot texture appears only in empty header slots, never behind text, and + typography stays a few sans-serif sizes, with capitals kept to + acronyms such as the camera's HDR and REC. + +Dark and Ocean fill the same roles, so the shell and every themed control +follow a theme switch without special cases. The camera and the game +panels keep fixed palettes of their own. + +The default build uses the desktop policy, so build the phone with the +mobile one, then run it with SDL (2x scaled by default; `MADO_SCALE=n` +overrides): + +```shell +$ make defconfig +$ python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y +$ python3 tools/kconfig/genconfig.py configs/Kconfig +$ make +$ ./demo-sdl # the phone +``` + +The default build runs the desktop demos; a `WM_NONE=y` build shows a +single app with no window manager. + +Drag the lock screen knob to unlock. Drag from the left edge of an app, or +press Escape, to go back. A hardware keyboard types into the focused +screen. In the desktop scene Alt+Tab cycles the windows. + +## Verification + +`make check` builds and runs the headless suites in-process: scan-out +lands in a memory framebuffer, time is a virtual clock, input is injected +through `twin_screen_dispatch()`. Mado is integer-only, so with the +built-in renderer every frame is bit-exact across hosts. + +- `test-core`: damage merging, compositor paths, shadows, opacity, stub + pixmaps, text, gradients, tweens, closure lifetime, scroll blitting, + multi-rectangle window damage, public accessors, leaks. +- `test-wm`: placement and input under the built policy, desktop activation, + focus after iconify/close, cycling, drag clamping, maximize, snap, + resize and releases outside the grab, windows destroying themselves from + their handlers, mobile push/pop/swipe/dismiss with mid-transition checks, + edge taps, back handling, bars hidden/replaced/destroyed/resized, the + keyboard inset, screen rotation and, with `WM_MOBILE_EVICT`, the memory + of covered windows. +- `test-lifetime`: callbacks that run while an object is torn down, or + that act on the object calling them. +- `test-demos`: every registry app except the phone shell (covered by + `test-cellphone`) under the built policy: placement, + consistency while it runs, repainting revealed windows, closing window + by window, a start that runs out of memory, leaks. +- `test-cellphone` (mobile builds): boots the phone with a fixed clock and + drives the whole user journey through every app, including the registry + apps opened from the launcher. + +The checks are layered: + +- **Differential.** Every optimization has a plain counterpart and must + produce identical bytes: occlusion culling on and off, coverage cache + cold, warm, disabled and starved, blit scrolling against a full repaint. +- **Damage consistency.** `th_damage_check()` recomposites the whole + screen from scratch with every optimization off and compares it with the + incrementally maintained frame. Any change that was not damaged shows up + as stale pixels. It runs after every navigation step and in the middle + of transitions. +- **Golden frames.** CRC-32 of key screens in `tests/golden/*.txt`, + tagged with a fingerprint of the rendering-relevant build options and + skipped when those differ. `make check-update` records new ones; + `MADO_SCREENSHOTS=dir` writes PPM screenshots of every golden frame. +- **Work and memory gates.** Heap returns to its baseline after app + cycles; covered screens cost no pixels; per-tick + repaint area of animated screens is bounded through `twin_screen_stats()`. + +CI runs the suites under the default configuration, Valgrind, +AddressSanitizer with UndefinedBehaviorSanitizer, and a configuration +matrix (no window manager, single policies, cache disabled, no widget +toolkit, RGB565 surfaces, TLSF heap, Pixman), plus an end-to-end run of +`demo-headless` driven through `headless-ctl` (now able to `type` text). diff --git a/include/twin.h b/include/twin.h index 06fe70b3..0f12ce3f 100644 --- a/include/twin.h +++ b/include/twin.h @@ -19,20 +19,15 @@ * graphics coordinates, colors, and timing values optimized for * embedded systems with minimal memory footprint. */ -typedef uint8_t twin_a8_t; /* 8-bit alpha channel value */ -typedef uint16_t twin_a16_t; /* 16-bit alpha channel value */ -typedef uint16_t twin_rgb16_t; /* 16-bit RGB color (5-6-5 format) */ -typedef uint32_t twin_argb32_t; /* 32-bit ARGB color with alpha */ -typedef uint32_t twin_ucs4_t; /* Unicode UCS-4 character code */ -typedef int16_t twin_coord_t; /* Screen/pixmap coordinate (-32K to 32K) */ -typedef int16_t twin_count_t; /* Count of items/elements */ -typedef int16_t twin_keysym_t; /* Keyboard symbol identifier */ -typedef uint8_t twin_js_number_t; /* Joystick controller number */ -typedef int16_t twin_js_value_t; /* Joystick axis/button value */ -typedef int32_t twin_area_t; /* Area measurement in pixels */ -typedef int32_t twin_time_t; /* Time value in milliseconds */ -typedef int16_t twin_stretch_t; /* Widget stretch factor for layout */ -typedef int32_t twin_fixed_t; /* Fixed-point number (16.16 format) */ +typedef uint8_t twin_a8_t; /* 8-bit alpha channel value */ +typedef uint32_t twin_argb32_t; /* 32-bit ARGB color with alpha */ +typedef uint32_t twin_ucs4_t; /* Unicode UCS-4 character code */ +typedef int16_t twin_coord_t; /* Screen/pixmap coordinate (-32K to 32K) */ +typedef int16_t twin_count_t; /* Count of items/elements */ +typedef int16_t twin_keysym_t; /* Keyboard symbol identifier */ +typedef int32_t twin_time_t; /* Time value in milliseconds */ +typedef int16_t twin_stretch_t; /* Widget stretch factor for layout */ +typedef int32_t twin_fixed_t; /* Fixed-point number (16.16 format) */ /** * Pixel format enumeration @@ -46,8 +41,6 @@ typedef enum { TWIN_ARGB32 /**< 32-bit ARGB with full alpha channel */ } twin_format_t; -#define twin_bytes_per_pixel(format) (1 << (twin_coord_t) (format)) - /* * Angles */ @@ -64,8 +57,6 @@ typedef int16_t twin_angle_t; #define TWIN_ANGLE_360 4096 #define TWIN_ANGLE_180 (TWIN_ANGLE_360 >> 1) #define TWIN_ANGLE_90 (TWIN_ANGLE_360 >> 2) -#define TWIN_ANGLE_45 (TWIN_ANGLE_360 >> 3) -#define TWIN_ANGLE_22_5 (TWIN_ANGLE_360 >> 4) #define TWIN_ANGLE_11_25 (TWIN_ANGLE_360 >> 5) #define TWIN_ANGLE_270 (TWIN_ANGLE_180 + TWIN_ANGLE_90) @@ -97,31 +88,11 @@ typedef struct _twin_matrix { twin_fixed_t m[3][2]; /**< 3x2 transformation matrix */ } twin_matrix_t; -/** - * Type-safe pointer union for different pixel formats - * - * Provides type-safe access to pixel data in different formats - * while maintaining memory efficiency through union storage. - */ -typedef union _twin_pointer { - void *v; /**< Generic void pointer */ - uint8_t *b; /**< Raw byte array pointer */ - twin_a8_t *a8; /**< 8-bit alpha format pointer */ - twin_rgb16_t *rgb16; /**< 16-bit RGB format pointer */ - twin_argb32_t *argb32; /**< 32-bit ARGB format pointer */ -} twin_pointer_t; - typedef struct _twin_window twin_window_t; typedef struct _twin_screen twin_screen_t; typedef struct _twin_pixmap twin_pixmap_t; typedef struct _twin_animation twin_animation_t; -/** Button signal types (used in unified event system) */ -typedef enum _twin_button_signal { - TwinButtonSignalDown /**< Sent when button pressed */, - TwinButtonSignalUp /**< Sent when button released inside widget */ -} twin_button_signal_t; - /** * Event type enumeration for input and system events * @@ -141,13 +112,11 @@ typedef enum _twin_event_kind { TwinEventKeyUp = 0x0102 /**< Keyboard key released */, TwinEventUcs4 = 0x0103 /**< Unicode character input */, - /* Focus */ + /* Focus and window management */ TwinEventActivate = 0x0201 /**< Window/widget activated */, TwinEventDeactivate = 0x0202 /**< Window/widget deactivated */, - - /* Joystick */ - TwinEventJoyButton = 0x0401 /**< Joystick button event */, - TwinEventJoyAxis = 0x0402 /**< Joystick axis movement */, + TwinEventClose = 0x0203 /**< Window manager asks the window to close */, + TwinEventBack = 0x0204 /**< Back requested: unhandled, the window closes */, /* Widgets */ TwinEventPaint = 0x1001 /**< Widget needs painting */, @@ -177,193 +146,30 @@ typedef struct _twin_event { struct { twin_keysym_t key; /**< Keyboard key symbol */ } key; /**< Keyboard event data */ - struct { - twin_js_number_t control; /**< Joystick controller ID */ - twin_js_value_t value; /**< Axis value or button state */ - } js; /**< Joystick event data */ struct { twin_ucs4_t ucs4; /**< Unicode character */ } ucs4; /**< Unicode character event data */ struct { twin_rect_t extents; /**< New widget geometry */ } configure; /**< Widget configuration event data */ - struct { - twin_button_signal_t signal; /**< Button signal type */ - } button_signal; /**< Button signal event data */ - } u; /**< Event-specific data union */ + } u; /**< Event-specific data union */ } twin_event_t; /** - * Event queue node for event processing + * Compositor counters * - * Linked list node used to queue events for asynchronous processing. - * Events are processed in FIFO order. - */ -typedef struct _twin_event_queue { - struct _twin_event_queue *next; /**< Next event in queue */ - twin_event_t event; /**< Event data */ -} twin_event_queue_t; - -/** - * Animation iterator for frame-by-frame playback - * - * 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. - */ -typedef 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_animation_t; - -/** - * Pixmap structure representing a rectangular array of pixels - * - * Core structure representing drawable surfaces with pixels. - * Can be displayed on screen or used as off-screen render targets. - */ -typedef struct _twin_pixmap { - /* Screen management */ - struct _twin_screen *screen; /**< Screen displaying this pixmap */ - twin_count_t disable; /**< Update disable counter */ - - /* Display list linkage */ - struct _twin_pixmap *down, *up; /**< Z-order linked list pointers */ - - /* Position and layout */ - twin_coord_t x, y; /**< Screen position */ - twin_format_t format; /**< Pixel format */ - twin_coord_t width, height; /**< Dimensions in pixels */ - twin_coord_t stride; /**< Row stride in bytes */ - twin_matrix_t transform; /**< 2D transformation matrix */ - - /* Clipping and origin */ - twin_rect_t clip; /**< Clipping rectangle */ - twin_coord_t origin_x, origin_y; /**< Drawing origin offset */ - - /* Pixel data */ - twin_animation_t *animation; /**< Animation data if animated */ - twin_pointer_t p; /**< Pixel data pointer */ - - bool shadow; /**< Drop shadow for active windows */ - - twin_window_t *window; /**< Associated window (if any) */ - - /* Transform buffer cache for compositing operations */ - void *xform_cache; /**< Cached xform buffer */ - size_t xform_cache_size; /**< Cached xform buffer size in bytes */ -} twin_pixmap_t; - -/** - * Scanline drawing callback called before drawing operation begins - * @left : Left edge of drawing region - * @top : Top edge of drawing region - * @right : Right edge of drawing region - * @bottom : Bottom edge of drawing region - * @closure : User data pointer - * - * Backend callback invoked before any scanlines are drawn. - * Used for setup operations like buffer preparation. - */ -typedef void (*twin_put_begin_t)(twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom, - void *closure); - -/** - * Scanline drawing callback for outputting pixel data - * @left : Left X coordinate of scanline - * @top : Y coordinate of scanline - * @right : Right X coordinate of scanline - * @pixels : Array of ARGB32 pixel data to draw - * @closure : User data pointer - * - * Backend callback invoked for each scanline to be drawn. - * Pixels array contains (right-left+1) ARGB32 values. - */ -typedef void (*twin_put_span_t)(twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_argb32_t *pixels, - void *closure); - -/** - * Screen structure managing display and window system - * - * Manages the display surface and coordinates drawing operations. - * Maintains Z-order of pixmaps and handles input event routing. - */ -struct _twin_screen { - /* Display list management */ - twin_pixmap_t *top, *bottom; /**< Z-order display list */ - twin_pixmap_t *active; /**< Active pixmap for key events */ - - /* Mouse event targeting */ - twin_pixmap_t *target; /**< Current mouse event target */ - bool clicklock; /**< Mouse click lock state */ - - /* Cursor management */ - twin_pixmap_t *cursor; /**< Mouse cursor pixmap */ - twin_coord_t curs_hx, curs_hy; /**< Cursor hotspot offset */ - twin_coord_t curs_x, curs_y; /**< Current cursor position */ - - /* Display properties */ - twin_coord_t width, height; /**< Screen dimensions */ - twin_pixmap_t *background; /**< Background pattern */ - - /* Damage tracking */ - twin_rect_t damage; /**< Current damage rectangle */ - void (*damaged)(void *); /**< Damage notification callback */ - void *damaged_closure; /**< Damage callback closure */ - twin_count_t disable; /**< Update disable counter */ - - /* Backend interface */ - twin_put_begin_t put_begin; /**< Scanline begin callback */ - twin_put_span_t put_span; /**< Scanline drawing callback */ - void *closure; /**< Backend user data */ - - /* Window manager */ - twin_coord_t button_x, button_y; /**< Window button position */ - - /* Span buffer cache for screen updates */ - twin_argb32_t *span_cache; /**< Cached span buffer */ - twin_coord_t span_cache_width; /**< Cached span buffer width */ - - /* Scratch arena for per-frame temporary allocations */ - void *scratch_buf; /**< Scratch buffer (bump allocator) */ - size_t scratch_size; /**< Scratch buffer capacity in bytes */ - - /* Reusable path object cache to avoid repeated alloc/free */ -#define TWIN_PATH_CACHE_SIZE 4 - struct _twin_path *path_cache[TWIN_PATH_CACHE_SIZE]; - int path_cache_count; - - /* Cached A8 mask pixmap for twin_composite_path -- avoids - * per-composite malloc for masks larger than the scratch arena. */ - twin_pixmap_t *mask_cache; - twin_coord_t mask_cache_width; - twin_coord_t mask_cache_height; - - /* Event processing: event filter callback */ - bool (*event_filter)(twin_screen_t *screen, twin_event_t *event); -}; + * Monotonic counters describing how much work the compositor performed. + * They are deterministic for a given scene and input sequence, which lets + * headless tests gate on work done rather than wall-clock time. + */ +typedef struct _twin_screen_stats { + uint32_t frames; /**< twin_screen_update calls that drew */ + uint32_t rects; /**< Damage rectangles scanned out */ + uint64_t pixels; /**< Pixels sent to the backend */ + uint64_t layer_pixels; /**< Pixels composited from layers */ + uint64_t occluded_pixels; /**< Layer pixels skipped by occlusion */ + uint64_t shadow_pixels; /**< Pixels touched by procedural shadows */ +} twin_screen_stats_t; /** * Source operand type for drawing operations @@ -397,16 +203,12 @@ typedef enum { #define TWIN_FIXED_ONE (0x10000) #define TWIN_FIXED_HALF (0x08000) -#define TWIN_FIXED_MAX (0x7fffffff) -#define TWIN_FIXED_MIN (-0x7fffffff) /* 'double' is a no-no in any shipping code, but useful during development */ #define twin_double_to_fixed(d) ((twin_fixed_t) ((d) * 65536)) -#define twin_fixed_to_double(f) ((double) (f) / 65536.0) -#define twin_int_to_fixed(i) ((twin_fixed_t) (i) << 16) -#define twin_fixed_ceil(f) (((f) + 0xffff) & ~0xffff) -#define twin_fixed_floor(f) ((f) & ~0xffff) +/* Multiply, not shift: left-shifting a negative value is undefined in C */ +#define twin_int_to_fixed(i) ((twin_fixed_t) (i) * 65536) #define twin_fixed_to_int(f) ((int) ((f) >> 16)) /** @@ -420,6 +222,7 @@ typedef struct _twin_point { } twin_point_t; typedef struct _twin_path twin_path_t; +typedef struct _twin_font twin_font_t; /** * Font style enumeration @@ -428,7 +231,6 @@ typedef enum _twin_style { TwinStyleRoman = 0 /**< Regular/normal font weight */, TwinStyleBold = 1 /**< Bold font weight */, TwinStyleOblique = 2 /**< Italic/oblique font style */, - TwinStyleBoldOblique = 3 /**< Bold italic combination */, TwinStyleUnhinted = 4 /**< Disable font hinting */, } twin_style_t; @@ -449,6 +251,7 @@ typedef enum _twin_cap { */ 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 */ @@ -470,61 +273,77 @@ typedef struct _twin_text_metrics { twin_fixed_t font_descent; /**< Font descent */ } twin_text_metrics_t; - /* * Fonts */ -#define UCS_PAGE_SHIFT 7 -#define UCS_PER_PAGE (1 << UCS_PAGE_SHIFT) - -static inline int twin_ucs_page(uint32_t ucs4) -{ - return ucs4 >> UCS_PAGE_SHIFT; -} - -static inline int twin_ucs_char_in_page(uint32_t ucs4) -{ - return ucs4 & (UCS_PER_PAGE - 1); -} - -typedef struct _twin_charmap { - unsigned int page; /**< Unicode page number */ - unsigned int offsets[UCS_PER_PAGE]; /**< Character offsets in page */ -} twin_charmap_t; - -#define TWIN_FONT_TYPE_STROKE 1 -#define TWIN_FONT_TYPE_TTF 2 - -typedef struct _twin_font { - /* Font definition - must be initialized */ - int type; /**< Font type (stroke or TTF) */ - const char *name; /**< Font name */ - const char *style; /**< Font style name */ - const twin_charmap_t *charmap; /**< Character map array */ - int n_charmap; /**< Number of character maps */ - const signed char *outlines; /**< Glyph outline data */ - signed char ascender; /**< Font ascender */ - signed char descender; /**< Font descender */ - signed char height; /**< Font height */ - - /* Runtime caching */ - const twin_charmap_t *cur_page; /**< Currently cached page */ -} twin_font_t; - -/* FIXME: one global font for now */ -extern twin_font_t *g_twin_font; - -/* Built-in default stroke font */ -extern twin_font_t twin_Default_Font_Roman; +#if defined(CONFIG_FONT_MONTSERRAT) +/* Montserrat Medium, Latin-1 printable subset, TrueType outlines */ +extern twin_font_t twin_font_montserrat; +#endif + +#if defined(CONFIG_FONT_ICONS) +/* UI symbol set (Font Awesome 5 Free Solid, CC BY 4.0) */ +extern twin_font_t twin_font_icons; + +/* UTF-8 strings for twin_text_draw(..., &twin_font_icons, ...) */ +#define TWIN_SYM_MUSIC "\xef\x80\x81" /* U+F001 */ +#define TWIN_SYM_USER "\xef\x80\x87" /* U+F007 */ +#define TWIN_SYM_CHECK "\xef\x80\x8c" /* U+F00C */ +#define TWIN_SYM_SIGNAL "\xef\x80\x92" /* U+F012 */ +#define TWIN_SYM_SETTINGS "\xef\x80\x93" /* U+F013 */ +#define TWIN_SYM_HOME "\xef\x80\x95" /* U+F015 */ +#define TWIN_SYM_CLOCK "\xef\x80\x97" /* U+F017 */ +#define TWIN_SYM_REPEAT "\xef\x80\x9e" /* U+F01E */ +#define TWIN_SYM_REFRESH "\xef\x80\xa1" /* U+F021 */ +#define TWIN_SYM_VOLUME "\xef\x80\xa8" /* U+F028 */ +#define TWIN_SYM_CAMERA "\xef\x80\xb0" /* U+F030 */ +#define TWIN_SYM_LIST "\xef\x80\xba" /* U+F03A */ +#define TWIN_SYM_IMAGE "\xef\x80\xbe" /* U+F03E */ +#define TWIN_SYM_PREV "\xef\x81\x88" /* U+F048 */ +#define TWIN_SYM_PLAY "\xef\x81\x8b" /* U+F04B */ +#define TWIN_SYM_PAUSE "\xef\x81\x8c" /* U+F04C */ +#define TWIN_SYM_NEXT "\xef\x81\x91" /* U+F051 */ +#define TWIN_SYM_LEFT "\xef\x81\x93" /* U+F053 */ +#define TWIN_SYM_RIGHT "\xef\x81\x94" /* U+F054 */ +#define TWIN_SYM_PLUS "\xef\x81\xa7" /* U+F067 */ +#define TWIN_SYM_SHUFFLE "\xef\x81\xb4" /* U+F074 */ +#define TWIN_SYM_MESSAGE "\xef\x81\xb5" /* U+F075 */ +#define TWIN_SYM_PHONE "\xef\x82\x95" /* U+F095 */ +#define TWIN_SYM_GLOBE "\xef\x82\xac" /* U+F0AC */ +#define TWIN_SYM_MAIL "\xef\x83\xa0" /* U+F0E0 */ +#define TWIN_SYM_BELL "\xef\x83\xb3" /* U+F0F3 */ +#define TWIN_SYM_KEYBOARD "\xef\x84\x9c" /* U+F11C */ +#define TWIN_SYM_MIC "\xef\x84\xb0" /* U+F130 */ +#define TWIN_SYM_SUN "\xef\x86\x85" /* U+F185 */ +#define TWIN_SYM_SEND "\xef\x87\x98" /* U+F1D8 */ +#define TWIN_SYM_HISTORY "\xef\x87\x9a" /* U+F1DA */ +#define TWIN_SYM_WIFI "\xef\x87\xab" /* U+F1EB */ +#define TWIN_SYM_CALCULATOR "\xef\x87\xac" /* U+F1EC */ +#define TWIN_SYM_BATTERY "\xef\x89\x80" /* U+F240 */ +#define TWIN_SYM_CONTACTS "\xef\x8a\xb9" /* U+F2B9 */ +#define TWIN_SYM_MOBILE "\xef\x8f\x8d" /* U+F3CD */ +#define TWIN_SYM_HANGUP "\xef\x8f\x9d" /* U+F3DD */ +#define TWIN_SYM_BACKSPACE "\xef\x95\x9a" /* U+F55A */ +#endif /* * Windows */ +/** + * Window styles describe intent; the active window manager decides geometry. + * + * - Plain: placed exactly as requested, never decorated (bars, popups) + * - Application: a managed app window. Desktop decorates it at the + * requested geometry; mobile stacks it over the work area; + * none maps it over the whole screen. + * - Fullscreen: covers the whole screen, above system bars, in every mode + */ typedef enum _twin_window_style { TwinWindowPlain /**< Plain window without decorations */, TwinWindowApplication /**< Standard application window */, + TwinWindowFullscreen /**< Undecorated window covering the screen */, } twin_window_style_t; typedef void (*twin_draw_func_t)(twin_window_t *window); @@ -533,58 +352,13 @@ typedef bool (*twin_event_func_t)(twin_window_t *window, twin_event_t *event); typedef void (*twin_destroy_func_t)(twin_window_t *window); -struct _twin_window { - /* Window basics */ - twin_screen_t *screen; /**< Parent screen */ - twin_pixmap_t *pixmap; /**< Window pixmap */ - - twin_coord_t shadow_x, shadow_y; /**< Shadow offset */ - - /* Window properties */ - twin_window_style_t style; /**< Window style */ - twin_rect_t client; /**< Client area rectangle */ - twin_rect_t damage; /**< Damage rectangle */ - - /* Window state */ - bool active; /**< Active window flag */ - bool iconify; /**< Iconified state */ - bool client_grab; /**< Mouse grab state */ - bool draw_queued; /**< Draw pending flag */ - - /* Window data */ - void *client_data; /**< User data pointer */ - char *name; /**< Window title */ - - /* Window callbacks */ - twin_draw_func_t draw; /**< Draw callback */ - twin_event_func_t event; /**< Event handler */ - twin_destroy_func_t destroy; /**< Destroy callback */ -}; - -/* - * Icons - */ - -typedef enum _twin_icon { - TwinIconMenu, - TwinIconIconify, - TwinIconRestore, - TwinIconClose, - TwinIconResize, -} twin_icon_t; - /* * Timeout and work procs return true to remain in the queue, * timeout procs are called every 'delay' ms */ typedef twin_time_t (*twin_timeout_proc_t)(twin_time_t now, void *closure); -typedef bool (*twin_work_proc_t)(void *closure); - -#define twin_time_compare(a, op, b) (((a) - (b)) op 0) - typedef struct _twin_timeout twin_timeout_t; -typedef struct _twin_work twin_work_t; /* * Widgets @@ -593,14 +367,6 @@ typedef struct _twin_work twin_work_t; typedef struct _twin_widget twin_widget_t; typedef struct _twin_box twin_box_t; -#define _twin_widget_width(w) \ - (((twin_widget_t *) (w))->extents.right - \ - ((twin_widget_t *) (w))->extents.left) - -#define _twin_widget_height(w) \ - (((twin_widget_t *) (w))->extents.bottom - \ - ((twin_widget_t *) (w))->extents.top) - typedef enum _twin_box_dir { TwinBoxHorz /**< Horizontal layout */, TwinBoxVert /**< Vertical layout */ @@ -608,8 +374,7 @@ typedef enum _twin_box_dir { typedef enum _twin_dispatch_result { TwinDispatchDone /**< Event processing complete */, - TwinDispatchContinue /**< Continue event propagation */, - TwinDispatchReject /**< Cannot handle this event */ + TwinDispatchContinue /**< Continue event propagation */ } twin_dispatch_result_t; /** @@ -621,7 +386,7 @@ typedef enum _twin_dispatch_result { * @widget : Widget receiving the event * @event : Event to process * @closure : User data pointer for stateful handling - * @return : Dispatch result (Done, Continue, or Reject) + * @return : Dispatch result (Done or Continue) * * This type is used for both framework-level handlers and application * callbacks. @@ -630,65 +395,13 @@ typedef twin_dispatch_result_t (*twin_widget_proc_t)(twin_widget_t *widget, twin_event_t *event, void *closure); -typedef struct _twin_widget_layout { - twin_coord_t width, height; /**< Preferred dimensions */ - twin_stretch_t stretch_width; /**< Horizontal stretch factor */ - twin_stretch_t stretch_height; /**< Vertical stretch factor */ -} twin_widget_layout_t; - typedef enum _twin_shape { TwinShapeRectangle /**< Rectangular shape */, - TwinShapeRoundedRectangle /**< Rectangle with rounded corners */, - TwinShapeLozenge /**< Lozenge/diamond shape */, - TwinShapeTab /**< Tab shape */, + TwinShapeLozenge /**< Rectangle with fully rounded ends */, TwinShapeEllipse /**< Elliptical shape */ } twin_shape_t; -/** - * Optional widget attributes, lazily allocated on first use. - * Widgets that never register a callback or request focus pay zero - * RAM cost for these fields. Allocated on first callback/focus use. - */ -typedef struct _twin_widget_ext { - twin_widget_proc_t callback; /**< Application callback (optional) */ - void *callback_data; /**< Callback user data */ - bool want_focus; /**< Focus request flag */ -} twin_widget_ext_t; - -struct _twin_widget { - /* Widget hierarchy */ - twin_window_t *window; /**< Parent window */ - twin_widget_t *next; /**< Next sibling widget */ - twin_box_t *parent; /**< Parent container */ - - /* Event handling */ - twin_widget_proc_t handler; /**< Widget event handler (framework) */ - twin_widget_ext_t *ext; /**< Optional attributes (lazy, may be NULL) */ - twin_rect_t extents; /**< Current geometry */ - twin_widget_t *copy_geom; /**< Geometry source widget (optional) */ - - /* Widget state */ - bool paint; /**< Needs painting */ - bool layout; /**< Needs layout */ - - /* Widget appearance */ - twin_argb32_t background; /**< Background color */ - twin_widget_layout_t preferred; /**< Preferred dimensions */ - twin_shape_t shape; /**< Widget shape */ - twin_fixed_t radius; /**< Corner radius */ -}; - -struct _twin_box { - twin_widget_t widget; /**< Base widget */ - twin_box_dir_t dir; /**< Layout direction */ - twin_widget_t *children; /**< Child widget list */ - twin_widget_t *button_down; /**< Button press target */ - twin_widget_t *focus; /**< Focused child */ -}; - -typedef struct _twin_toplevel { - twin_box_t box; /**< Base box container */ -} twin_toplevel_t; +typedef struct _twin_toplevel twin_toplevel_t; typedef enum _twin_align { TwinAlignLeft /**< Left alignment */, @@ -696,29 +409,11 @@ typedef enum _twin_align { TwinAlignRight /**< Right alignment */ } twin_align_t; -typedef struct _twin_label { - twin_widget_t widget; /**< Base widget */ - char *label; /**< Label text */ - twin_argb32_t foreground; /**< Text color */ - twin_fixed_t font_size; /**< Font size */ - twin_style_t font_style; /**< Font style */ - twin_point_t offset; /**< Text offset */ - twin_align_t align; /**< Text alignment */ -} twin_label_t; +typedef struct _twin_label twin_label_t; typedef struct _twin_button twin_button_t; - -struct _twin_button { - twin_label_t label; /**< Base label widget */ - bool pressed; /**< Button pressed state */ - bool active; /**< Button active state */ -}; - -typedef struct _twin_context { - twin_screen_t *screen; /**< Screen context */ - void *priv; /**< Private backend data */ -} twin_context_t; +typedef struct _twin_context twin_context_t; /** * Create a container widget for organizing child widgets @@ -745,7 +440,6 @@ twin_button_t *twin_button_create(twin_box_t *parent, twin_fixed_t font_size, twin_style_t font_style); - /** * Set application callback for widget events (low-level API) * @widget : Widget to configure @@ -754,29 +448,12 @@ twin_button_t *twin_button_create(twin_box_t *parent, * * Allows applications to respond to widget events (e.g., button clicks). * The callback receives events after the widget's core handler processes them. + * Returns false when it could not be installed (out of memory). */ -void twin_widget_set_callback(twin_widget_t *widget, +bool twin_widget_set_callback(twin_widget_t *widget, twin_widget_proc_t callback, void *data); -/** - * Set whether a widget should take focus on pointer interaction. - * @widget : Widget to configure - * @focusable : true to route key/UCS4 events to this widget after click - * - * This is primarily used by custom widgets that handle keyboard input. - */ -void twin_widget_set_focusable(twin_widget_t *widget, bool focusable); - -/** - * Create default mouse cursor pixmap - * @hx : Output hotspot X coordinate - * @hy : Output hotspot Y coordinate - * - * Return Cursor pixmap with arrow design, or NULL on failure - */ -twin_pixmap_t *twin_make_cursor(int *hx, int *hy); - /** * Start the Twin application with initialization callback * @ctx : Twin context containing screen and backend data @@ -793,7 +470,7 @@ twin_pixmap_t *twin_make_cursor(int *hx, int *hy); * * This is the recommended way to start Twin applications. */ -void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *)); +void twin_run(twin_context_t *ctx, void (*init_callback)(twin_screen_t *)); /** * Composite source onto destination with optional mask @@ -847,73 +524,23 @@ void twin_fill(twin_pixmap_t *dst, twin_coord_t right, twin_coord_t bottom); -/** - * Apply stack blur effect to rectangular region - * @px : Pixmap to blur (must be TWIN_ARGB32 format) - * @radius : Blur radius (larger values create stronger blur) - * @left : Left edge of blur region - * @right : Right edge of blur region (inclusive) - * @top : Top edge of blur region - * @bottom : Bottom edge of blur region (inclusive) - * - * Applies a fast stack blur algorithm to create soft shadows, - * gaussian-like blur effects, and smooth transitions. The blur - * only affects the specified rectangular region. - */ -void twin_stack_blur(twin_pixmap_t *px, - int radius, - twin_coord_t left, - twin_coord_t right, - twin_coord_t top, - twin_coord_t bottom); - -/** - * Add event to processing queue - * @event : Event to be queued for processing - * - * Enqueues events for asynchronous processing by the dispatch loop. - * Events are processed in FIFO order. - */ - #define twin_fixed_mul(a, b) ((twin_fixed_t) (((int64_t) (a) * (b)) >> 16)) -#define twin_fixed_div(a, b) ((twin_fixed_t) ((((int64_t) (a)) << 16) / (b))) +/* a / b in 16.16. When |a| * 65536 fits in 31 bits a 32-bit divide gives + * the same quotient (no INT32_MIN / -1 either), which spares 32-bit targets + * a 64-bit division */ +static inline twin_fixed_t twin_fixed_div(twin_fixed_t a, twin_fixed_t b) +{ + /* Two 32-bit cases give the 64-bit quotient exactly: a small dividend + * shifts without overflow, and a whole-number divisor cancels the + * shift (except -1, whose quotient of INT32_MIN overflows) */ + if ((uint32_t) a + 0x7fffu < 0xffffu) + return (twin_fixed_t) ((int32_t) ((uint32_t) a << 16) / b); + if (!(b & 0xffff) && b != -TWIN_FIXED_ONE) + return a / (b >> 16); + return (twin_fixed_t) (((int64_t) a * 65536) / b); +} #define twin_fixed_abs(f) ((f) < 0 ? -(f) : (f)) -/** - * Calculate square root of fixed-point number - * @a : Input value in fixed-point format - * - * Return Square root in fixed-point format - */ -twin_fixed_t twin_fixed_sqrt(twin_fixed_t a); - -/** - * Check if font contains specified Unicode character - * @font : Font to check - * @ucs4 : Unicode character code - * - * Return True if character is available in font - */ -bool twin_has_ucs4(twin_font_t *font, twin_ucs4_t ucs4); - -/** - * Add stroked outline of Unicode character to path - * @path : Path to add character outline to - * @ucs4 : Unicode character to add - * - * Creates outlined/stroked version of character for unfilled text. - */ -void twin_path_ucs4_stroke(twin_path_t *path, twin_ucs4_t ucs4); - -/** - * Add filled outline of Unicode character to path - * @path : Path to add character to - * @ucs4 : Unicode character to add - * - * Adds character outline to path for filled text rendering. - */ -void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4); - /** * Add UTF-8 string outline to path * @path : Path to add text to @@ -923,36 +550,6 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4); */ void twin_path_utf8(twin_path_t *path, const char *string); -/** - * Get advance width of Unicode character - * @path : Path with current font settings - * @ucs4 : Unicode character - * - * Return Character advance width in fixed-point format - */ -twin_fixed_t twin_width_ucs4(twin_path_t *path, twin_ucs4_t ucs4); - -/** - * Get total width of UTF-8 string - * @path : Path with current font settings - * @string : UTF-8 encoded string - * - * Return Total string width in fixed-point format - */ -twin_fixed_t twin_width_utf8(twin_path_t *path, const char *string); - -/** - * Get detailed metrics for Unicode character - * @path : Path with current font settings - * @ucs4 : Unicode character - * @m : Output metrics structure - * - * Fills metrics structure with detailed typography information. - */ -void twin_text_metrics_ucs4(twin_path_t *path, - twin_ucs4_t ucs4, - twin_text_metrics_t *m); - /** * Get detailed metrics for UTF-8 string * @path : Path with current font settings @@ -965,16 +562,13 @@ void twin_text_metrics_utf8(twin_path_t *path, const char *string, twin_text_metrics_t *m); -void twin_icon_draw(twin_pixmap_t *pixmap, - twin_icon_t icon, - twin_matrix_t matrix); - -twin_pixmap_t *twin_pixmap_from_file(const char *path, twin_format_t fmt); +/** Load a PNG, JPEG, GIF or TinyVG file (as built) into an ARGB32 pixmap */ +twin_pixmap_t *twin_pixmap_from_file(const char *path); /* - * Defines the interface for managing frame-based animations. - * It provides functions to control and manipulate animations such as getting - * the current frame, advancing the animation, and releasing resources. + * 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. */ /* Get the number of milliseconds the current frame should be displayed. */ @@ -987,41 +581,29 @@ twin_pixmap_t *twin_animation_get_current_frame(const twin_animation_t *anim); * will return to the first frame after the last one. */ void twin_animation_advance_frame(twin_animation_t *anim); -/* Frees the memory allocated for the animation, including all associated - * frames. - */ -void twin_animation_destroy(twin_animation_t *anim); - -twin_animation_iter_t *twin_animation_iter_init(twin_animation_t *anim); - -void twin_animation_iter_advance(twin_animation_iter_t *iter); - twin_label_t *twin_label_create(twin_box_t *parent, const char *value, twin_argb32_t foreground, twin_fixed_t font_size, twin_style_t font_style); -void twin_label_set(twin_label_t *label, - const char *value, - twin_argb32_t foreground, - twin_fixed_t font_size, - twin_style_t font_style); +/** Change a label's text */ +void twin_label_set(twin_label_t *label, const char *value); -void twin_matrix_identity(twin_matrix_t *m); +void twin_label_set_align(twin_label_t *label, twin_align_t align); -bool twin_matrix_is_identity(twin_matrix_t *m); +/** The widget a box, label or button is built on, for the twin_widget_* + * calls */ +twin_widget_t *twin_box_widget(twin_box_t *box); +twin_widget_t *twin_label_widget(twin_label_t *label); +twin_widget_t *twin_button_widget(twin_button_t *button); -void twin_matrix_translate(twin_matrix_t *m, twin_fixed_t tx, twin_fixed_t ty); +void twin_matrix_identity(twin_matrix_t *m); void twin_matrix_scale(twin_matrix_t *m, twin_fixed_t sx, twin_fixed_t sy); void twin_matrix_rotate(twin_matrix_t *m, twin_angle_t a); -void twin_matrix_multiply(twin_matrix_t *result, - const twin_matrix_t *a, - const twin_matrix_t *b); - /** * Transform coordinates using a transformation matrix. * @@ -1031,43 +613,21 @@ void twin_matrix_multiply(twin_matrix_t *result, */ /** - * Transform the X coordinate using the given matrix. + * Transform a point by a matrix * @m : Transformation matrix to apply - * @x : Input X coordinate in fixed-point format - * @y : Input Y coordinate in fixed-point format - * @return : Transformed X coordinate in fixed-point format - * - * Applies the matrix transformation to compute the new X coordinate. - * Both input coordinates are required as 2D transformations can - * affect both X and Y components. - */ -twin_fixed_t twin_matrix_transform_x(const twin_matrix_t *m, - twin_fixed_t x, - twin_fixed_t y); - -/** - * Transform the Y coordinate using the given matrix. - * @m : Transformation matrix to apply - * @x : Input X coordinate in fixed-point format - * @y : Input Y coordinate in fixed-point format - * @return : Transformed Y coordinate in fixed-point format - * - * Applies the matrix transformation to compute the new Y coordinate. - * Both input coordinates are required as 2D transformations can - * affect both X and Y components. + * @x, @y : Point in fixed-point format + * @ox, @oy: Where the transformed point goes */ -twin_fixed_t twin_matrix_transform_y(const twin_matrix_t *m, - twin_fixed_t x, - twin_fixed_t y); +void twin_matrix_transform(const twin_matrix_t *m, + twin_fixed_t x, + twin_fixed_t y, + twin_fixed_t *ox, + twin_fixed_t *oy); void twin_path_move(twin_path_t *path, twin_fixed_t x, twin_fixed_t y); -void twin_path_rmove(twin_path_t *path, twin_fixed_t x, twin_fixed_t y); - void twin_path_draw(twin_path_t *path, twin_fixed_t x, twin_fixed_t y); -void twin_path_rdraw(twin_path_t *path, twin_fixed_t x, twin_fixed_t y); - void twin_path_circle(twin_path_t *path, twin_fixed_t x, twin_fixed_t y, @@ -1098,15 +658,6 @@ void twin_path_arc_ellipse(twin_path_t *path, twin_fixed_t target_y, twin_angle_t rotation); -void twin_path_arc_circle(twin_path_t *path, - bool large_arc, - bool sweep, - twin_fixed_t radius, - twin_fixed_t cur_x, - twin_fixed_t cur_y, - twin_fixed_t target_x, - twin_fixed_t target_y); - void twin_path_rectangle(twin_path_t *path, twin_fixed_t x, twin_fixed_t y, @@ -1121,64 +672,34 @@ void twin_path_rounded_rectangle(twin_path_t *path, twin_fixed_t x_radius, twin_fixed_t y_radius); -void twin_path_lozenge(twin_path_t *path, - twin_fixed_t x, - twin_fixed_t y, - twin_fixed_t w, - twin_fixed_t h); - -void twin_path_tab(twin_path_t *path, - twin_fixed_t x, - twin_fixed_t y, - twin_fixed_t w, - twin_fixed_t h, - twin_fixed_t x_radius, - twin_fixed_t y_radius); - - void twin_path_close(twin_path_t *path); void twin_path_empty(twin_path_t *path); -void twin_path_bounds(twin_path_t *path, twin_rect_t *rect); - -void twin_path_append(twin_path_t *dst, twin_path_t *src); - twin_path_t *twin_path_create(void); void twin_path_destroy(twin_path_t *path); -void twin_path_identity(twin_path_t *path); - void twin_path_translate(twin_path_t *path, twin_fixed_t tx, twin_fixed_t ty); void twin_path_scale(twin_path_t *path, twin_fixed_t sx, twin_fixed_t sy); void twin_path_rotate(twin_path_t *path, twin_angle_t a); -twin_matrix_t twin_path_current_matrix(twin_path_t *path); - void twin_path_set_matrix(twin_path_t *path, twin_matrix_t matrix); -twin_fixed_t twin_path_current_font_size(twin_path_t *path); - void twin_path_set_font_size(twin_path_t *path, twin_fixed_t font_size); -twin_style_t twin_path_current_font_style(twin_path_t *path); - 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); -twin_cap_t twin_path_current_cap_style(twin_path_t *path); - twin_state_t twin_path_save(twin_path_t *path); void twin_path_restore(twin_path_t *path, twin_state_t *state); void twin_paint_path(twin_pixmap_t *dst, twin_argb32_t argb, twin_path_t *path); - void twin_paint_stroke(twin_pixmap_t *dst, twin_argb32_t argb, twin_path_t *stroke, @@ -1186,8 +707,6 @@ void twin_paint_stroke(twin_pixmap_t *dst, twin_pixmap_t *twin_make_pattern(void); -#define twin_pixmap_is_animated(pix) ((pix)->animation != NULL) - /** * Create pixmap with allocated pixel buffer * @format : Pixel format (A8, RGB16, or ARGB32) @@ -1200,22 +719,6 @@ twin_pixmap_t *twin_pixmap_create(twin_format_t format, twin_coord_t width, twin_coord_t height); -/** - * Create pixmap using existing pixel buffer - * @format : Pixel format - * @width : Width in pixels - * @height : Height in pixels - * @stride : Row stride in bytes - * @pixels : Existing pixel buffer (not copied) - * - * Return Pixmap wrapper for existing buffer, or NULL on failure - */ -twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, - twin_coord_t width, - twin_coord_t height, - twin_coord_t stride, - twin_pointer_t pixels); - /** * Destroy pixmap and free resources * @pixmap : Pixmap to destroy @@ -1224,25 +727,29 @@ twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, */ void twin_pixmap_destroy(twin_pixmap_t *pixmap); -void twin_pixmap_show(twin_pixmap_t *pixmap, - twin_screen_t *screen, - twin_pixmap_t *higher); - -void twin_pixmap_hide(twin_pixmap_t *pixmap); - -void twin_pixmap_enable_update(twin_pixmap_t *pixmap); - -void twin_pixmap_disable_update(twin_pixmap_t *pixmap); - -void twin_pixmap_get_origin(twin_pixmap_t *pixmap, - twin_coord_t *ox, - twin_coord_t *oy); +twin_coord_t twin_pixmap_width(const twin_pixmap_t *pixmap); +twin_coord_t twin_pixmap_height(const twin_pixmap_t *pixmap); -void twin_pixmap_set_origin(twin_pixmap_t *pixmap, - twin_coord_t ox, - twin_coord_t oy); +/** + * Direct access to the pixel buffer + * @pixmap : Pixmap to access + * @stride : Receives the row stride in bytes (may be NULL) + * + * Return the first pixel row, or NULL when the pixmap holds no pixels. + * Meant for off-screen pixmaps: drawing calls track screen damage, direct + * writes do not. + */ +void *twin_pixmap_pixels(twin_pixmap_t *pixmap, twin_coord_t *stride); +/** Frames of an animated image, or NULL for a still pixmap */ +twin_animation_t *twin_pixmap_animation(const twin_pixmap_t *pixmap); +/** + * Set the transform applied when the pixmap is a composite source + * @pixmap : Source pixmap + * @m : Destination-to-source matrix, NULL for the identity + */ +void twin_pixmap_set_transform(twin_pixmap_t *pixmap, const twin_matrix_t *m); void twin_pixmap_clip(twin_pixmap_t *pixmap, twin_coord_t left, @@ -1250,93 +757,19 @@ void twin_pixmap_clip(twin_pixmap_t *pixmap, twin_coord_t right, twin_coord_t bottom); -void twin_pixmap_set_clip(twin_pixmap_t *pixmap, twin_rect_t clip); - - twin_rect_t twin_pixmap_get_clip(twin_pixmap_t *pixmap); twin_rect_t twin_pixmap_save_clip(twin_pixmap_t *pixmap); void twin_pixmap_restore_clip(twin_pixmap_t *pixmap, twin_rect_t rect); -void twin_pixmap_reset_clip(twin_pixmap_t *pixmap); - -void twin_pixmap_damage(twin_pixmap_t *pixmap, - twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom); - - - -void twin_pixmap_move(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y); - - -bool twin_pixmap_transparent(twin_pixmap_t *pixmap, - twin_coord_t x, - twin_coord_t y); - -bool twin_pixmap_is_iconified(twin_pixmap_t *pixmap, twin_coord_t y); - -bool twin_pixmap_dispatch(twin_pixmap_t *pixmap, twin_event_t *event); - -/** - * Create screen for window system display - * @width : Screen width in pixels - * @height : Screen height in pixels - * @put_begin : Backend callback for drawing preparation - * @put_span : Backend callback for scanline output - * @closure : User data passed to backend callbacks - * - * Return Newly created screen, or NULL on failure - */ -twin_screen_t *twin_screen_create(twin_coord_t width, - twin_coord_t height, - twin_put_begin_t put_begin, - twin_put_span_t put_span, - void *closure); - -/** - * Destroy screen and free resources - * @screen : Screen to destroy - * - * Cleans up screen resources including all associated pixmaps. - */ -void twin_screen_destroy(twin_screen_t *screen); - - - -void twin_screen_damage(twin_screen_t *screen, - twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom); - - -void twin_screen_resize(twin_screen_t *screen, - twin_coord_t width, - twin_coord_t height); - -bool twin_screen_damaged(twin_screen_t *screen); - -void twin_screen_update(twin_screen_t *screen); - -void twin_screen_set_active(twin_screen_t *screen, twin_pixmap_t *pixmap); - -twin_pixmap_t *twin_screen_get_active(twin_screen_t *screen); - void twin_screen_set_background(twin_screen_t *screen, twin_pixmap_t *pixmap); -twin_pixmap_t *twin_screen_get_background(twin_screen_t *screen); - -void twin_screen_set_cursor(twin_screen_t *screen, - twin_pixmap_t *pixmap, - twin_fixed_t hotspot_x, - twin_fixed_t hotspot_y); - -bool twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event); - +twin_coord_t twin_screen_width(const twin_screen_t *screen); +twin_coord_t twin_screen_height(const twin_screen_t *screen); +/** Compositor work counters since the screen was created */ +const twin_screen_stats_t *twin_screen_stats(const twin_screen_t *screen); /* * spline.c @@ -1370,9 +803,12 @@ void twin_clear_timeout(twin_timeout_t *timeout); * toplevel.c */ +/** + * A toplevel's box starts with an opaque white background, so the window + * is opaque. A translucent background on the box (twin_box_widget with + * twin_widget_set) or a non-rectangular shape makes it translucent again. + */ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, - twin_format_t format, - twin_window_style_t style, twin_coord_t x, twin_coord_t y, twin_coord_t width, @@ -1381,51 +817,29 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, void twin_toplevel_show(twin_toplevel_t *toplevel); +/** Destroy a toplevel with its window and every widget in it */ +void twin_toplevel_destroy(twin_toplevel_t *toplevel); + +/** The box that holds a toplevel's widgets */ +twin_box_t *twin_toplevel_box(twin_toplevel_t *toplevel); + /* * trig.c */ -twin_fixed_t twin_sin(twin_angle_t a); - -twin_fixed_t twin_cos(twin_angle_t a); - -twin_fixed_t twin_tan(twin_angle_t a); - void twin_sincos(twin_angle_t a, twin_fixed_t *sin, twin_fixed_t *cos); -twin_angle_t twin_atan2(twin_fixed_t y, twin_fixed_t x); - -twin_angle_t twin_acos(twin_fixed_t x); - /* * widget.c */ - -twin_widget_t *twin_widget_create(twin_box_t *parent, - twin_argb32_t background, - twin_coord_t width, - twin_coord_t height, - twin_stretch_t hstretch, - twin_stretch_t vstretch); - void twin_widget_set(twin_widget_t *widget, twin_argb32_t background); -/* Get widget dimensions */ -twin_fixed_t twin_widget_width(twin_widget_t *widget); -twin_fixed_t twin_widget_height(twin_widget_t *widget); - -/* Request widget repaint */ -void twin_widget_queue_paint(twin_widget_t *widget); +/** Outline the widget background is painted with */ +void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape); -/* Create widget with custom event handler */ -twin_widget_t *twin_widget_create_with_handler(twin_box_t *parent, - twin_argb32_t background, - twin_coord_t width, - twin_coord_t height, - twin_stretch_t hstretch, - twin_stretch_t vstretch, - twin_widget_proc_t handler); +/** Make @widget request the same preferred size as @source */ +void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source); /* * Custom widget support - allows creating widgets without accessing internals @@ -1435,14 +849,16 @@ twin_widget_t *twin_widget_create_with_handler(twin_box_t *parent, * access to twin_private.h or internal widget structures. */ +typedef struct _twin_custom_widget twin_custom_widget_t; + /** - * Structure representing a custom widget with user data. - * Encapsulates a base widget and allows attaching custom data. + * Event handler of a custom widget: called after the toolkit's own + * handling, except TwinEventDestroy, which reaches the handler first, + * while the widget and its data are still intact, so the handler can + * release what the data points to */ -typedef struct { - twin_widget_t *widget; /**< Base widget functionality */ - void *data; /**< User-defined data */ -} twin_custom_widget_t; +typedef twin_dispatch_result_t ( + *twin_custom_proc_t)(twin_custom_widget_t *custom, twin_event_t *event); /** * Create a custom widget with user-defined data and event handler. @@ -1451,44 +867,21 @@ typedef struct { * @background : Background color (ARGB32 format) * @width : Preferred width in pixels (0 for flexible) * @height : Preferred height in pixels (0 for flexible) - * @hstretch : Horizontal stretch factor for layout - * @vstretch : Vertical stretch factor for layout - * @handler : Custom event handler function for this widget + * @handler : Event handler for this widget (may be NULL) * @data_size : Size of custom data to allocate (0 for no data) * @return : Newly created custom widget, or NULL on failure * - * The handler function will be called for all events sent to this widget. - * Custom data (if requested) is zero-initialized and accessible via - * twin_custom_widget_data(). + * The widget stretches to fill its share of the box. Custom data (if + * requested) is zero-initialized and accessible via + * twin_custom_widget_data(); it is freed with the widget. */ twin_custom_widget_t *twin_custom_widget_create(twin_box_t *parent, twin_argb32_t background, twin_coord_t width, twin_coord_t height, - twin_stretch_t hstretch, - twin_stretch_t vstretch, - twin_widget_proc_t handler, + twin_custom_proc_t handler, size_t data_size); -/** - * Get the drawing pixmap from a widget for rendering operations. - * - * @widget : Widget to get pixmap from - * @return : Widget's pixmap for drawing, or NULL if invalid - */ -twin_pixmap_t *twin_widget_pixmap(twin_widget_t *widget); - -/** - * Retrieve the custom widget wrapper from a base widget. - * - * @widget : Base widget to look up - * @return : Associated custom widget, or NULL if not found - * - * This function allows event handlers to retrieve their custom widget - * context when receiving events through the base widget dispatch system. - */ -twin_custom_widget_t *twin_widget_get_custom(twin_widget_t *widget); - /** * Get the user data pointer from a custom widget. * @@ -1500,28 +893,17 @@ twin_custom_widget_t *twin_widget_get_custom(twin_widget_t *widget); */ void *twin_custom_widget_data(twin_custom_widget_t *custom); -/** - * Get the base widget from a custom widget. - * - * @custom : Custom widget to get base widget from - * @return : Base widget, or NULL if invalid - * - * Provides access to the underlying widget for operations that require - * the base widget interface. - */ -twin_widget_t *twin_custom_widget_base(twin_custom_widget_t *custom); - /* * Convenience functions for common custom widget operations. * These functions provide simplified access to frequently needed operations * without requiring direct manipulation of the base widget. */ -/** Get the current width of a custom widget */ -twin_fixed_t twin_custom_widget_width(twin_custom_widget_t *custom); +/** Get the current width of a custom widget, in pixels */ +twin_coord_t twin_custom_widget_width(twin_custom_widget_t *custom); -/** Get the current height of a custom widget */ -twin_fixed_t twin_custom_widget_height(twin_custom_widget_t *custom); +/** Get the current height of a custom widget, in pixels */ +twin_coord_t twin_custom_widget_height(twin_custom_widget_t *custom); /** Request that a custom widget be repainted */ void twin_custom_widget_queue_paint(twin_custom_widget_t *custom); @@ -1568,41 +950,297 @@ void twin_window_configure(twin_window_t *window, twin_coord_t width, twin_coord_t height); +/** Title shown in the title bar; only the desktop manager keeps it */ void twin_window_set_name(twin_window_t *window, const char *name); +/** The pixmap a window draws into; it includes any decorations */ +twin_pixmap_t *twin_window_pixmap(const twin_window_t *window); + +/** The client area in pixmap coordinates */ +twin_rect_t twin_window_client(const twin_window_t *window); + +twin_window_style_t twin_window_style(const twin_window_t *window); + +/** True once twin_window_show() was called and until twin_window_hide() */ +bool twin_window_is_mapped(const twin_window_t *window); + +/** + * Install the client callbacks + * @draw : Repaints the client area (may be NULL) + * @event : Receives input and window events (may be NULL) + * @destroy : Runs before the window is freed (may be NULL) + */ +void twin_window_set_callbacks(twin_window_t *window, + twin_draw_func_t draw, + twin_event_func_t event, + twin_destroy_func_t destroy); + +void twin_window_set_client_data(twin_window_t *window, void *data); +void *twin_window_client_data(const twin_window_t *window); + void twin_window_draw(twin_window_t *window); +/** Mark a client rectangle (pixmap coordinates) for repainting; draw() + * runs for it on the next pass */ void twin_window_damage(twin_window_t *window, twin_coord_t left, twin_coord_t top, twin_coord_t right, twin_coord_t bottom); -void twin_window_queue_paint(twin_window_t *window); +/** + * Ask a window to close + * @window : Window to close + * + * Delivers TwinEventClose to the window's event handler. If the handler + * does not consume it, the window is destroyed. Window managers use the + * same path for title-bar close buttons and back gestures, so applications + * handle closing in one place. The mobile manager animates the departure + * from a detached copy of the pixels, so the window's state may be freed + * immediately. + */ +void twin_window_close(twin_window_t *window); + +/** + * Ask a window to go back + * @window : Window to ask + * + * Delivers TwinEventBack, so an application with its own history can step + * back inside the window; if the handler does not consume it, the window + * is closed with twin_window_close(). The mobile manager sends it for the + * edge swipe and the Escape key. + */ +void twin_window_back(twin_window_t *window); + +/** + * Raise a mapped window and give it the keyboard, through the active + * window manager (desktop also restores an iconified window; mobile brings + * the application back to the top of the stack). Bars are left alone. + */ +void twin_window_activate(twin_window_t *window); + +/** + * Declare that draw() paints every pixel of the client area + * @window : Window to update + * @opaque : true when the client area is always fully opaque + * + * Lets the compositor skip whatever lies underneath (occlusion culling). + * RGB16 windows are always opaque. + */ +void twin_window_set_opaque(twin_window_t *window, bool opaque); + +/** + * Scroll part of a window's client area without repainting it + * @window : Window to scroll + * @area : Region in client coordinates + * @dx, dy : Displacement in pixels + * + * Moves the retained pixels inside @area and adds only the exposed band + * to the window damage, then queues a paint. Damage still pending inside + * @area moves with the pixels. Update the state that paint() reads (the + * scroll offset) before calling this. This is how lists scroll at full + * speed on CPUs without graphics acceleration. + */ +void twin_window_scroll(twin_window_t *window, + twin_rect_t area, + twin_coord_t dx, + twin_coord_t dy); + +/* + * Window manager + */ + +/* + * The window manager is chosen when the library is built: the Kconfig + * "Window manager" choice selects the desktop policy (CONFIG_WM_DESKTOP: + * overlapping, decorated, draggable windows), the mobile one + * (CONFIG_WM_MOBILE: a full-screen app stack between system bars, with + * transitions) or none (CONFIG_WM_NONE: no decorations, application + * windows cover the screen, the newest or last pressed window has the + * keyboard). Every screen runs that policy for its whole life. + */ + +/** + * Register system bars (status bar, navigation bar) + * @screen : Screen + * @top : Plain window pinned above the work area, or NULL + * @bottom : Plain window pinned below the work area, or NULL + * + * The mobile manager shows the bars and shrinks the work area between + * them by the height of each mapped bar; the others have no system bars + * and just hide the windows. The + * screen does not own the bars: a bar that is replaced is unmapped, and a + * destroyed bar unregisters itself. Resizing a bar with + * twin_window_configure() re-lays out the applications. + */ +void twin_screen_set_bars(twin_screen_t *screen, + twin_window_t *top, + twin_window_t *bottom); + +/** + * Reserve rows at the bottom of the work area, e.g. for an on-screen + * keyboard + * @screen : Screen + * @bottom : Rows to reserve above the bottom bar (0 releases them) + * + * Application windows are re-laid out (they receive TwinEventConfigure). + * Only the mobile manager has a work area to shrink; the others ignore it. + */ +void twin_screen_set_inset(twin_screen_t *screen, twin_coord_t bottom); + +/** + * Area available to application windows under the active policy + */ +twin_rect_t twin_screen_work_area(const twin_screen_t *screen); + +/** + * Raise and activate the lowest managed window, cycling through all of + * them on repeated calls (desktop Alt-Tab, mobile app switching) + */ +void twin_screen_focus_next(twin_screen_t *screen); + +/** + * Walk the screen's windows, bottom to top in stacking order: + * for (w = twin_screen_first_window(s); w; w = twin_window_next(w)) + * The list includes hidden and system-bar windows. Destroying @w inside + * the loop is allowed when the next window was fetched first. + */ +twin_window_t *twin_screen_first_window(const twin_screen_t *screen); +twin_window_t *twin_window_next(const twin_window_t *window); + +/* + * Compositing attributes + */ + +/* + * Text + * + * Text is drawn glyph by glyph from the coverage cache; each glyph is + * rasterized once per (font, size) and composited from then on. + */ + +/** + * Draw UTF-8 text + * @dst : Destination pixmap (clip and origin apply) + * @font : Font, or NULL for g_twin_font + * @size : Font size (em height) in pixels, fixed point + * @x, y : Pen position of the baseline, fixed point + * @color : Premultiplied ARGB color + * @utf8 : Text to draw + * + * Return the advance (pen displacement) in fixed point. + */ +twin_fixed_t twin_text_draw(twin_pixmap_t *dst, + twin_font_t *font, + twin_fixed_t size, + twin_fixed_t x, + twin_fixed_t y, + twin_argb32_t color, + const char *utf8); + +/** Advance width of @utf8 as twin_text_draw() would lay it out */ +twin_fixed_t twin_text_width(twin_font_t *font, + twin_fixed_t size, + const char *utf8); + +/* + * Gradients + */ + +/** + * Fill a path with a two-stop linear gradient + * @dst : Destination pixmap + * @path : Shape to fill + * @x0, y0 : Start point (color @c0), path coordinates + * @x1, y1 : End point (color @c1) + * + * Colors are premultiplied ARGB and are clamped beyond the end points. + * Vertical gradients cost one solid fill per scanline. + */ +void twin_paint_path_linear(twin_pixmap_t *dst, + twin_path_t *path, + twin_fixed_t x0, + twin_fixed_t y0, + twin_argb32_t c0, + twin_fixed_t x1, + twin_fixed_t y1, + twin_argb32_t c1); + +/* + * Coverage cache + * + * Rasterized coverage masks are kept in one byte-budgeted LRU shared by + * glyphs and small filled paths. Paths are keyed by their device-space + * outline relative to the pixel grid, so a shape drawn again at another + * integer position hits the cache. A budget of 0 disables caching. + */ +typedef struct _twin_cache_stats { + uint32_t hits; /**< Lookups served from the cache */ + uint32_t misses; /**< Lookups that had to rasterize */ + uint32_t evictions; /**< Entries dropped to honor the budget */ + uint32_t entries; /**< Entries currently resident */ + size_t bytes; /**< Bytes currently resident */ + size_t peak_bytes; /**< High-water mark of resident bytes */ + size_t budget; /**< Configured budget in bytes */ +} twin_cache_stats_t; + +void twin_cache_get_stats(twin_cache_stats_t *stats); + +/* + * Tweens: time-based property animation + */ + +typedef enum _twin_ease { + TwinEaseLinear, + TwinEaseOutCubic, + TwinEaseInOutCubic, + TwinEaseOutBack, +} twin_ease_t; -#define TWIN_WORK_REDISPLAY 0 -#define TWIN_WORK_PAINT 1 -#define TWIN_WORK_LAYOUT 2 +typedef struct _twin_tween twin_tween_t; -twin_work_t *twin_set_work(twin_work_proc_t work_proc, - int priority, - void *closure); +/** + * Tween step callback + * @value : Eased progress, 0 to TWIN_FIXED_ONE (may overshoot for OutBack) + * @closure : User data + * + * Called once per frame; the final call passes TWIN_FIXED_ONE exactly, + * after which the tween is freed. Return false to stop early. + */ +typedef bool (*twin_tween_proc_t)(twin_fixed_t value, void *closure); -void twin_clear_work(twin_work_t *work); +/** + * Start a tween + * @duration : Length in milliseconds (0 finishes on the first frame) + * @ease : Easing curve + * @proc : Step callback + * @closure : User data + * + * Progress is derived from elapsed time, so dropped frames shorten the + * animation instead of slowing it down. + */ +twin_tween_t *twin_tween_start(twin_time_t duration, + twin_ease_t ease, + twin_tween_proc_t proc, + void *closure); -twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath, - twin_format_t fmt, - twin_coord_t w, - twin_coord_t h); +/** Stop a tween without calling its step callback again */ +void twin_tween_cancel(twin_tween_t *tween); -twin_pixmap_t *twin_tvg_to_pixmap_budget(const char *filepath, - twin_format_t fmt, - size_t memory_budget); +/** Monotonic time in milliseconds, the clock used by timeouts and tweens */ +twin_time_t twin_now(void); -twin_pixmap_t *twin_pixmap_create_budget(twin_format_t format, - twin_coord_t max_width, - twin_coord_t max_height, - size_t memory_budget); +/** + * 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 + * + * The pixmap shrinks further, keeping its aspect, to fit + * CONFIG_TVG_MEMORY_BUDGET when that is set. + */ +twin_pixmap_t *twin_tvg_to_pixmap(const char *filepath, + twin_coord_t w, + twin_coord_t h); /** * Memory usage statistics (requires CONFIG_MEMORY_STATS) @@ -1615,7 +1253,6 @@ typedef struct _twin_memory_info { } twin_memory_info_t; void twin_memory_get_info(twin_memory_info_t *info); -void twin_memory_reset_peak(void); twin_context_t *twin_create(int width, int height); diff --git a/mk/common.mk b/mk/common.mk index 57221c52..7ba5893e 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -311,7 +311,9 @@ _cmd_link.cxx.host = $(HOSTCXX) $(__HOSTARCHFLAGS) $(__CXXFLAGS) $($1_lib _cmd_link_so.cxx.host = $(HOSTCXX) $(__HOSTARCHFLAGS) $(__CXXFLAGS) $($1_libraries) -o $$@ $$(filter-out __FORCE $($1_depends_y),$$^) \ $($1_archflags) $($1_ldflags) $$(filter-out -static,$(__LDFLAGS)) -shared $$(if $($1_soname-y),-Wl$$(comma)-soname$$(comma)$($1_soname-y),) -_cmd_ar = $(AR) rcs $$@ $$(filter-out __FORCE $($1_depends_y),$$^) +# Start the archive afresh: members of a previous configuration (another +# window manager defines the same symbols) must not linger +_cmd_ar = $(RM) $$@ && $(AR) rcs $$@ $$(filter-out __FORCE $($1_depends_y),$$^) _cmd_ranlib = $(RANLIB) $$@ _cmd_ld = $(LD) $(LDOPTS) -r -o $$@ $$(filter-out __FORCE $($1_depends_y),$$^) @@ -678,8 +680,9 @@ clean: __FORCE __FORCE: @true -# Only include dependencies when building known targets -build-goals := all clean $(target-builds) +# Only include dependencies when building known targets. Projects add +# goals that build targets (e.g. a test runner) through extra-build-goals. +build-goals := all clean $(target-builds) $(extra-build-goals) ifneq ($(MAKECMDGOALS),) # MAKECMDGOALS is not empty, check if ALL goals are known # (i.e., no unknown goals remain after filtering out known ones) diff --git a/scripts/gen-composite-decls.py b/scripts/gen-composite-decls.py index a10f77f8..6a6b882d 100755 --- a/scripts/gen-composite-decls.py +++ b/scripts/gen-composite-decls.py @@ -2,11 +2,11 @@ """ Generate composite function declarations for twin_private.h -This script generates the ~128 function declarations for compositing operations -that are currently manually listed in twin_private.h:280-405. +This script generates the declarations of the compositing functions: +72 three-operand and 24 two-operand ones, plus the screen and vector forms. -The functions are actually generated by macros in src/primitive.c, but we need -their declarations in the header. +The functions are generated by macros in src/draw-builtin.c; the build +writes these declarations to src/composite-decls.h. """ # Format types for src, msk, and dst @@ -32,7 +32,8 @@ def generate_3operand_decls(): for op in OPS: for dst in ["argb32", "rgb16", "a8"]: # dst doesn't include 'c' for src in FORMATS: - for msk in FORMATS: + # An RGB16 mask has no alpha: compositing treats it as none + for msk in ["argb32", "a8", "c"]: func_name = f"_twin_{src}_in_{msk}_{op}_{dst}" decls.append(f"twin_in_op_func {func_name};") @@ -125,7 +126,7 @@ def main(): print(decl) print("\n/* 3-operand compositing: _twin_{src}_in_{msk}_{op}_{dst} */") - print("/* Total: 2 ops * 3 dsts * 4 srcs * 4 msks = 96 functions */") + print("/* Total: 2 ops * 3 dsts * 4 srcs * 3 msks = 72 functions */") print("/* Only available when CONFIG_RENDERER_BUILTIN=y */") for decl in generate_3operand_decls(): print(decl) diff --git a/scripts/gen-shadow-lut.py b/scripts/gen-shadow-lut.py deleted file mode 100755 index 69d6fe4b..00000000 --- a/scripts/gen-shadow-lut.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate precomputed Gaussian-style weights for the drop shadow LUT. - -The curve approximates (1 - t^2)^2 * 0.92 + 0.08 sampled at 17 points -between 0 and 1 (inclusive). Values are emitted in 16.16 fixed-point so -the renderer can remain purely fixed-point. -""" - -import math -import pathlib -import sys - - -def compute_weights(samples: int = 16) -> list[int]: - weights = [] - for idx in range(samples + 1): - t = idx / samples - value = ((1.0 - t * t) ** 2) * 0.92 + 0.08 - fixed = int(round(value * 65536)) # 16.16 fixed-point - fixed = max(0, min(fixed, 0xFFFFFFFF)) - weights.append(fixed) - return weights - - -HEADER_TEMPLATE = """\ -/* - * This file is auto-generated by scripts/gen-shadow-lut.py. - * Do not edit manually. - */ -#ifndef SHADOW_GAUSSIAN_LUT_H -#define SHADOW_GAUSSIAN_LUT_H - -#include "twin.h" - -static const twin_fixed_t shadow_gaussian_lut[{count}] = {{ -{body} -}}; - -#endif /* SHADOW_GAUSSIAN_LUT_H */ -""" - - -def emit_header(path: pathlib.Path) -> None: - weights = compute_weights() - lines = [] - for idx, value in enumerate(weights): - suffix = "," if idx < len(weights) - 1 else "" - lines.append(f" 0x{value:05x}{suffix}") - header = HEADER_TEMPLATE.format(count=len(weights), body="\n".join(lines)) - path.write_text(header, encoding="utf-8") - - -def main(argv: list[str]) -> int: - if len(argv) != 2: - sys.stderr.write("Usage: gen-shadow-lut.py \n") - return 1 - output_path = pathlib.Path(argv[1]) - emit_header(output_path) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv)) diff --git a/src/anim.c b/src/anim.c new file mode 100644 index 00000000..0ec78ac1 --- /dev/null +++ b/src/anim.c @@ -0,0 +1,142 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Tweens: time-based animation on top of timeouts + * + * Progress comes from elapsed time, not from counting frames, so a slow + * frame shortens the animation rather than stretching it. All arithmetic + * is 16.16 fixed point. + */ + +#include "twin_private.h" + +#define TWIN_TWEEN_FRAME_MS 16 + +struct _twin_tween { + twin_timeout_t *timeout; + twin_time_t start, duration; + twin_ease_t ease; + twin_tween_proc_t proc; + void *closure; + bool running; /* inside proc: defer frees */ + bool dead; /* cancelled or finished from inside proc */ + bool finished; /* finished from inside proc: deliver the last frame */ +}; + +twin_fixed_t twin_ease(twin_ease_t ease, twin_fixed_t t) +{ + if (t <= 0) + return 0; + if (t >= TWIN_FIXED_ONE) + return TWIN_FIXED_ONE; + + switch (ease) { + case TwinEaseOutCubic: { + twin_fixed_t u = TWIN_FIXED_ONE - t; + return TWIN_FIXED_ONE - twin_fixed_mul(twin_fixed_mul(u, u), u); + } + case TwinEaseInOutCubic: + if (t < TWIN_FIXED_HALF) + return 4 * twin_fixed_mul(twin_fixed_mul(t, t), t); + else { + twin_fixed_t u = 2 * (TWIN_FIXED_ONE - t); + return TWIN_FIXED_ONE - twin_fixed_mul(twin_fixed_mul(u, u), u) / 2; + } + case TwinEaseOutBack: { + /* 1 + c3 (t - 1)^3 + c1 (t - 1)^2, c1 = 1.70158, c3 = c1 + 1 */ + const twin_fixed_t c1 = 111514; /* 1.70158 */ + const twin_fixed_t c3 = c1 + TWIN_FIXED_ONE; + twin_fixed_t u = t - TWIN_FIXED_ONE; + twin_fixed_t u2 = twin_fixed_mul(u, u); + return TWIN_FIXED_ONE + twin_fixed_mul(c3, twin_fixed_mul(u2, u)) + + twin_fixed_mul(c1, u2); + } + case TwinEaseLinear: + default: + return t; + } +} + +static twin_time_t _twin_tween_tick(twin_time_t now, void *closure) +{ + twin_tween_t *tween = closure; + twin_time_t elapsed = + (twin_time_t) ((uint32_t) now - (uint32_t) tween->start); + /* A tween of no duration ends at once, even on a tick stamped early */ + bool last = elapsed >= tween->duration || tween->duration <= 0; + twin_fixed_t value; + + if (last) + value = TWIN_FIXED_ONE; + else + /* elapsed / duration in 16.16; under 32 s it needs no 64-bit + * division */ + value = + twin_ease(tween->ease, twin_fixed_div(elapsed, tween->duration)); + + tween->running = true; + bool keep = (*tween->proc)(value, tween->closure); + /* twin_tween_finish() from inside proc still promises the end value, + * even when proc also returns false to stop */ + if (tween->finished && !last) + (*tween->proc)(TWIN_FIXED_ONE, tween->closure); + tween->running = false; + + if (last || !keep || tween->dead) { + twin_free(tween); + return -1; /* drop the timeout */ + } + return TWIN_TWEEN_FRAME_MS; +} + +twin_tween_t *twin_tween_start(twin_time_t duration, + twin_ease_t ease, + twin_tween_proc_t proc, + void *closure) +{ + twin_tween_t *tween = twin_calloc(1, sizeof(*tween)); + if (!tween) + return NULL; + tween->start = twin_now(); + tween->duration = duration > 0 ? duration : 0; + tween->ease = ease; + tween->proc = proc; + tween->closure = closure; + tween->timeout = twin_set_timeout(_twin_tween_tick, 0, tween); + if (!tween->timeout) { + twin_free(tween); + return NULL; + } + return tween; +} + +void twin_tween_cancel(twin_tween_t *tween) +{ + if (!tween) + return; + if (tween->running) { + tween->dead = true; + return; + } + twin_clear_timeout(tween->timeout); + twin_free(tween); +} + +void twin_tween_finish(twin_tween_t *tween) +{ + if (!tween) + return; + if (tween->running) { + tween->dead = true; + tween->finished = true; + return; + } + twin_clear_timeout(tween->timeout); + tween->running = true; + (*tween->proc)(TWIN_FIXED_ONE, tween->closure); + twin_free(tween); +} diff --git a/src/animation.c b/src/animation.c index 4afd5091..1fe39958 100644 --- a/src/animation.c +++ b/src/animation.c @@ -21,31 +21,42 @@ twin_pixmap_t *twin_animation_get_current_frame(const twin_animation_t *anim) 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]; +} + void twin_animation_advance_frame(twin_animation_t *anim) { if (!anim) return; - twin_animation_iter_advance(anim->iter); + _twin_animation_iter_advance(anim->iter); } -void twin_animation_destroy(twin_animation_t *anim) +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++) { + 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) +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 || !anim) + if (!iter) return NULL; iter->current_index = 0; iter->current_frame = anim->frames[0]; @@ -54,18 +65,3 @@ twin_animation_iter_t *twin_animation_iter_init(twin_animation_t *anim) iter->anim = anim; return iter; } - -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) { - if (anim->loop) { - iter->current_index = 0; - } else { - iter->current_index = anim->n_frames - 1; - } - } - iter->current_frame = anim->frames[iter->current_index]; - iter->current_delay = anim->frame_delays[iter->current_index]; -} diff --git a/src/api.c b/src/api.c index b2ef82b7..bff56eb2 100644 --- a/src/api.c +++ b/src/api.c @@ -4,62 +4,34 @@ * All rights reserved. */ -#include #include #include "twin_private.h" -extern twin_backend_t g_twin_backend; - -/** - * Create a new Twin context with specified dimensions. - * - * Initializes the backend and creates a screen context for rendering. - * - * @width : Screen width in pixels - * @height : Screen height in pixels - * @return : Twin context, or NULL on failure - */ twin_context_t *twin_create(int width, int height) { /* Initialize the memory pool before any allocations occur. */ twin_mem_pool_init(); - /* Runtime check for missing backend */ - if (!g_twin_backend.init) { - log_error("Backend not registered - no init function"); - return NULL; - } - - assert(g_twin_backend.init && "Backend not registered"); - twin_context_t *ctx = g_twin_backend.init(width, height); if (!ctx) { log_error("Backend initialization failed (%dx%d)", width, height); + return NULL; } +#if defined(CONFIG_CURSOR) + /* Backends that draw no cursor of their own get the arrow */ + twin_pixmap_t *cursor = _twin_make_cursor(); + if (cursor) + twin_screen_set_cursor(ctx->screen, cursor, 0, 0); +#endif return ctx; } -/** - * Destroy a Twin context and release all resources. - * - * Cleans up backend resources, frees memory, and terminates the context. - * - * @ctx : Twin context to destroy - */ void twin_destroy(twin_context_t *ctx) { if (!ctx) return; - /* Runtime check for missing backend */ - if (!g_twin_backend.exit) { - log_error("Backend not registered - no exit function"); - return; - } - - assert(g_twin_backend.exit && "Backend not registered"); - /* Screen teardown may emit damage callbacks through backend-owned closure. */ if (ctx->screen) { @@ -70,6 +42,9 @@ void twin_destroy(twin_context_t *ctx) /* Backend frees its private data and ctx */ g_twin_backend.exit(ctx); + /* Cached coverage outlives screens; release it with the system */ + twin_cache_flush(); + /* Report memory statistics after all teardown */ twin_memory_info_t mem; twin_memory_get_info(&mem); @@ -81,34 +56,24 @@ void twin_destroy(twin_context_t *ctx) mem.total_frees); } -/** - * Start the Twin application with initialization callback. - * - * This function provides a platform-independent way to start Twin applications. - * It delegates to the backend's start function, which handles platform-specific - * main loop setup. - * - * @ctx : Twin context to run - * @init_callback : Application initialization function (called once before - * event loop) - */ -void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *)) +bool twin_dispatch_once(twin_context_t *ctx) { - /* Validate context parameter */ - if (!ctx) { - log_error("NULL context passed to twin_run"); - return; - } - - assert(ctx && "NULL context passed to twin_run"); + _twin_run_timeout(); + _twin_run_work(); + /* The backend sleeps here until input or the next thing to do */ + return g_twin_backend.poll(ctx); +} - /* Runtime check for missing start function */ - if (!g_twin_backend.start) { - log_error("Backend has no start function - main loop not running"); +void twin_run(twin_context_t *ctx, void (*init_callback)(twin_screen_t *)) +{ + if (!ctx) + return; + if (g_twin_backend.start) { + g_twin_backend.start(ctx, init_callback); return; } - - assert(g_twin_backend.start && "Backend start function not registered"); - - g_twin_backend.start(ctx, init_callback); + if (init_callback) + init_callback(ctx->screen); + while (twin_dispatch_once(ctx)) + ; } diff --git a/src/box.c b/src/box.c index d13c233e..fd90ed22 100644 --- a/src/box.c +++ b/src/box.c @@ -19,7 +19,6 @@ void _twin_box_init(twin_box_t *box, box->dir = dir; box->children = NULL; box->button_down = NULL; - box->focus = NULL; } static twin_dispatch_result_t _twin_box_query_geometry(twin_box_t *box) @@ -147,6 +146,31 @@ static twin_widget_t *_twin_box_xy_to_widget(twin_box_t *box, return NULL; } +void _twin_box_mark_paint(twin_box_t *box) +{ + box->widget.paint = true; + for (twin_widget_t *child = box->children; child; child = child->next) { + child->paint = true; + if (child->handler == _twin_box_dispatch) + _twin_box_mark_paint((twin_box_t *) child); + } +} + +/* Does @widget repaint every pixel it covers? A box marked only because + * a descendant is does not. */ +static bool _twin_widget_repaints_whole(const twin_widget_t *widget) +{ + if (!widget->paint) + return false; + if (widget->handler != _twin_box_dispatch) + return true; + for (const twin_widget_t *c = ((const twin_box_t *) widget)->children; c; + c = c->next) + if (!_twin_widget_repaints_whole(c)) + return false; + return true; +} + twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure) @@ -165,9 +189,11 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, child = box->children; box->children = child->next; - /* Send destroy event to child */ + /* Send destroy event to child, then release it: the box owns + * the widgets created inside it */ ev.kind = TwinEventDestroy; child->handler(child, &ev, _twin_widget_callback_data(child)); + twin_free(child); } break; case TwinEventQueryGeometry: @@ -178,8 +204,6 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_window_show(widget->window); box->button_down = _twin_box_xy_to_widget(box, event->u.pointer.x, event->u.pointer.y); - if (box->button_down && _twin_widget_want_focus(box->button_down)) - box->focus = box->button_down; fallthrough; case TwinEventButtonUp: case TwinEventMotion: @@ -192,14 +216,14 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, _twin_widget_callback_data(child)); } break; - case TwinEventKeyDown: - case TwinEventKeyUp: - case TwinEventUcs4: - if (box->focus) - return box->focus->handler(box->focus, event, - _twin_widget_callback_data(box->focus)); - break; - case TwinEventPaint: + case TwinEventPaint: { + /* When every child repaints whole, so does the box: fill it first, + * so the gaps between children show its background */ + bool whole = true; + for (child = box->children; child && whole; child = child->next) + whole = _twin_widget_repaints_whole(child); + if (whole) + _twin_widget_paint(widget); box->widget.paint = false; for (child = box->children; child; child = child->next) if (child->paint) { @@ -208,11 +232,6 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_coord_t ox, oy; twin_pixmap_get_origin(pixmap, &ox, &oy); - if (child->shape != TwinShapeRectangle) - twin_fill(child->window->pixmap, widget->background, - TWIN_SOURCE, child->extents.left, - child->extents.top, child->extents.right, - child->extents.bottom); twin_pixmap_set_clip(pixmap, child->extents); twin_pixmap_origin_to_clip(pixmap); child->paint = false; @@ -221,6 +240,7 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_pixmap_set_origin(pixmap, ox, oy); } break; + } default: break; } @@ -236,3 +256,8 @@ twin_box_t *twin_box_create(twin_box_t *parent, twin_box_dir_t dir) _twin_box_init(box, parent, 0, dir, _twin_box_dispatch); return box; } + +twin_widget_t *twin_box_widget(twin_box_t *box) +{ + return &box->widget; +} diff --git a/src/button.c b/src/button.c index d3f29967..7c5aea76 100644 --- a/src/button.c +++ b/src/button.c @@ -49,7 +49,6 @@ twin_dispatch_result_t _twin_button_dispatch(twin_widget_t *widget, if (_twin_widget_callback(widget)) { twin_event_t press_event = *event; press_event.kind = TwinEventButtonSignalDown; - press_event.u.button_signal.signal = TwinButtonSignalDown; _twin_widget_callback(widget)(widget, &press_event, _twin_widget_callback_data(widget)); } @@ -76,7 +75,6 @@ twin_dispatch_result_t _twin_button_dispatch(twin_widget_t *widget, if (_twin_widget_callback(widget)) { twin_event_t release_event = *event; release_event.kind = TwinEventButtonSignalUp; - release_event.u.button_signal.signal = TwinButtonSignalUp; _twin_widget_callback(widget)( widget, &release_event, _twin_widget_callback_data(widget)); } @@ -119,3 +117,8 @@ twin_button_t *twin_button_create(twin_box_t *parent, _twin_button_dispatch); return button; } + +twin_widget_t *twin_button_widget(twin_button_t *button) +{ + return &button->label.widget; +} diff --git a/src/cache.c b/src/cache.c new file mode 100644 index 00000000..d01542c4 --- /dev/null +++ b/src/cache.c @@ -0,0 +1,181 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Coverage cache + * + * Anti-aliased rasterization dominates the cost of redrawing vector UI: + * every glyph and rounded rectangle is scan-converted with 4x4 supersampling. + * Most of that work repeats. A list row, a keypad, or a home screen redraws + * the same few dozen shapes, often at different positions. + * + * This cache keeps A8 coverage masks in a single LRU with a byte budget. + * Glyph entries are keyed by (font, size, code point); path entries by a + * hash of the device-space outline relative to the pixel grid, so the same + * shape at another integer offset hits. Because the rasterizer's sample grid + * is pixel-aligned, a cached mask is bit-identical to a fresh one. + * + * Memory is bounded by the budget plus one hash table; entries larger than + * CONFIG_CACHE_MAX_ENTRY are never admitted, so a full-screen fill cannot + * flush the working set. + */ + +#include "twin_private.h" + +#define TWIN_CACHE_BUCKETS 128 /* power of two */ + +typedef struct { + twin_cache_entry_t *buckets[TWIN_CACHE_BUCKETS]; + twin_cache_entry_t *head, *tail; /* most / least recently used */ + size_t bytes, peak_bytes; + uint32_t entries, hits, misses, evictions; +} twin_cache_t; + +/* The table starts zeroed in .bss; only the budget needs an initial value */ +static twin_cache_t cache; +static size_t cache_budget = (size_t) CONFIG_CACHE_SIZE * 1024; + +static inline unsigned _bucket(uint64_t key) +{ + return (unsigned) ((key ^ (key >> 29) ^ (key >> 47)) & + (TWIN_CACHE_BUCKETS - 1)); +} + +static void _lru_unlink(twin_cache_entry_t *e) +{ + if (e->prev) + e->prev->next = e->next; + else + cache.head = e->next; + if (e->next) + e->next->prev = e->prev; + else + cache.tail = e->prev; + e->prev = e->next = NULL; +} + +static void _lru_push_front(twin_cache_entry_t *e) +{ + e->prev = NULL; + e->next = cache.head; + if (cache.head) + cache.head->prev = e; + cache.head = e; + if (!cache.tail) + cache.tail = e; +} + +static void _cache_remove(twin_cache_entry_t *e) +{ + twin_cache_entry_t **pp = &cache.buckets[_bucket(e->key)]; + while (*pp && *pp != e) + pp = &(*pp)->hnext; + if (*pp) + *pp = e->hnext; + _lru_unlink(e); + cache.bytes -= e->size; + cache.entries--; + twin_free(e); +} + +static void _cache_shrink(size_t target) +{ + while (cache.tail && cache.bytes > target) { + _cache_remove(cache.tail); + cache.evictions++; + } +} + +twin_cache_entry_t *_twin_cache_entry_alloc(twin_coord_t width, + twin_coord_t height) +{ + twin_coord_t stride = _twin_a8_stride(width); + twin_cache_entry_t *e = + twin_calloc(1, sizeof(twin_cache_entry_t) + (size_t) stride * height); + if (!e) + return NULL; + e->width = width; + e->height = height; + e->stride = stride; + return e; +} + +bool _twin_cache_enabled(void) +{ + return cache_budget > 0; +} + +twin_cache_entry_t *_twin_cache_lookup(uint64_t key) +{ + /* Disabled: the cache is not consulted, so this is neither a hit nor a + * miss (path masks skip the lookup outright, see _twin_cache_enabled) */ + if (!cache_budget) + return NULL; + for (twin_cache_entry_t *e = cache.buckets[_bucket(key)]; e; e = e->hnext) { + if (e->key == key) { + if (e != cache.head) { + _lru_unlink(e); + _lru_push_front(e); + } + cache.hits++; + return e; + } + } + cache.misses++; + return NULL; +} + +twin_cache_entry_t *_twin_cache_insert(uint64_t key, + twin_coord_t width, + twin_coord_t height) +{ + if (!cache_budget || width < 0 || height < 0) + return NULL; + size_t data = (size_t) _twin_a8_stride(width) * (size_t) height; + size_t size = sizeof(twin_cache_entry_t) + data; + if (data > CONFIG_CACHE_MAX_ENTRY || size > cache_budget) + return NULL; + + _cache_shrink(cache_budget - size); + twin_cache_entry_t *e = _twin_cache_entry_alloc(width, height); + if (!e) + return NULL; + e->key = key; + e->size = (uint32_t) size; + + unsigned b = _bucket(key); + e->hnext = cache.buckets[b]; + cache.buckets[b] = e; + _lru_push_front(e); + cache.bytes += size; + cache.entries++; + if (cache.bytes > cache.peak_bytes) + cache.peak_bytes = cache.bytes; + return e; +} + +void twin_cache_set_budget(size_t bytes) +{ + cache_budget = bytes; + _cache_shrink(bytes); +} + +void twin_cache_flush(void) +{ + while (cache.tail) + _cache_remove(cache.tail); +} + +void twin_cache_get_stats(twin_cache_stats_t *stats) +{ + stats->hits = cache.hits; + stats->misses = cache.misses; + stats->evictions = cache.evictions; + stats->entries = cache.entries; + stats->bytes = cache.bytes; + stats->peak_bytes = cache.peak_bytes; + stats->budget = cache_budget; +} diff --git a/src/closure.c b/src/closure.c index d4582fb4..91d3b938 100644 --- a/src/closure.c +++ b/src/closure.c @@ -35,22 +35,42 @@ * 4. Work execution: _twin_closure_is_valid() check prevents use-after-free */ +#include #include #include "twin_private.h" -/* Global closure tracker instance */ -twin_closure_tracker_t _twin_closure_tracker = {0}; - -/* - * Initialize the closure tracking system. - * Called once during screen initialization to set up the tracking table. +/* Global closure tracker instance, live from program start so timeouts set + * before any screen are tracked. + * TODO: add a mutex when threading support is added */ -void _twin_closure_tracker_init(void) +twin_closure_tracker_t _twin_closure_tracker = { + .entries = _twin_closure_tracker.inline_entries, + .capacity = TWIN_CLOSURE_INLINE, +}; + +/* Make room for one more entry, spilling to the heap when inline is full */ +static bool _twin_closure_reserve(void) { - memset(&_twin_closure_tracker, 0, sizeof(_twin_closure_tracker)); - _twin_closure_tracker.initialized = true; - /* TODO: Initialize mutex when threading support is added */ + twin_closure_tracker_t *t = &_twin_closure_tracker; + if (t->count < t->capacity) + return true; + if ((size_t) t->capacity > INT_MAX / 2 / sizeof(*t->entries)) + return false; + int cap = t->capacity * 2; + twin_closure_entry_t *e; + if (t->entries == t->inline_entries) { + e = twin_malloc(cap * sizeof(*e)); + if (e) + memcpy(e, t->entries, t->count * sizeof(*e)); + } else { + e = twin_realloc(t->entries, cap * sizeof(*e)); + } + if (!e) + return false; + t->entries = e; + t->capacity = cap; + return true; } /* @@ -94,10 +114,6 @@ bool _twin_closure_register(void *closure) if (!twin_pointer_valid(closure)) return false; - /* If tracker not initialized, skip registration */ - if (!_twin_closure_tracker.initialized) - return true; /* Pretend success if tracking not yet enabled */ - /* Check if already registered */ twin_closure_entry_t *entry = _twin_closure_find_entry(closure); if (entry) { @@ -106,8 +122,7 @@ bool _twin_closure_register(void *closure) return true; } - /* Check capacity */ - if (_twin_closure_tracker.count >= TWIN_MAX_CLOSURES) + if (!_twin_closure_reserve()) return false; /* Add new entry at the end */ @@ -127,20 +142,22 @@ bool _twin_closure_register(void *closure) */ void _twin_closure_unregister(void *closure) { + twin_closure_tracker_t *t = &_twin_closure_tracker; if (!closure) return; - for (int i = 0; i < _twin_closure_tracker.count; i++) { - if (_twin_closure_tracker.entries[i].closure == closure) { - /* Swap with last entry and decrement count */ - if (i < _twin_closure_tracker.count - 1) { - _twin_closure_tracker.entries[i] = - _twin_closure_tracker - .entries[_twin_closure_tracker.count - 1]; - } - _twin_closure_tracker.count--; - return; + for (int i = 0; i < t->count; i++) { + if (t->entries[i].closure != closure) + continue; + /* Fill the hole with the last entry */ + t->entries[i] = t->entries[--t->count]; + /* An empty table hands its heap back */ + if (!t->count && t->entries != t->inline_entries) { + twin_free(t->entries); + t->entries = t->inline_entries; + t->capacity = TWIN_CLOSURE_INLINE; } + return; } } @@ -153,10 +170,6 @@ void _twin_closure_unregister(void *closure) */ bool _twin_closure_ref(void *closure) { - /* Skip if tracker not initialized */ - if (!_twin_closure_tracker.initialized) - return true; - twin_closure_entry_t *entry = _twin_closure_find_entry(closure); if (!entry || entry->marked_for_free) return false; @@ -167,23 +180,24 @@ bool _twin_closure_ref(void *closure) /* * Decrement the reference count for a closure. - * Called when work items complete or timeouts are cleared. - * - * Note: We don't auto-unregister at zero refs to maintain explicit - * ownership semantics. The owner must call unregister during destruction. + * Called when work items complete or timeouts are cleared; the entry goes + * away with its last reference. */ bool _twin_closure_unref(void *closure) { - /* Skip if tracker not initialized */ - if (!_twin_closure_tracker.initialized) - return true; - twin_closure_entry_t *entry = _twin_closure_find_entry(closure); if (!entry) return false; + /* Drop the entry with its last holder so the table only ever tracks + * closures that still have pending timeouts or work. Otherwise every + * distinct closure ever scheduled leaks a slot until the table fills + * and scheduling starts failing. + */ if (entry->ref_count > 0) entry->ref_count--; + if (entry->ref_count == 0) + _twin_closure_unregister(closure); return true; } @@ -210,10 +224,6 @@ bool _twin_closure_is_valid(void *closure) if (!twin_pointer_valid(closure)) return false; - /* If tracker not initialized, fall back to basic pointer validation */ - if (!_twin_closure_tracker.initialized) - return true; /* Assume valid if tracking not yet enabled */ - /* Must be tracked to be valid */ twin_closure_entry_t *entry = _twin_closure_find_entry(closure); if (!entry) diff --git a/src/cursor.c b/src/cursor.c index 49b2e351..78040f36 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -108,15 +108,8 @@ const uint8_t _twin_cursor_default[] = { 0x00, 0x00, 0x00, 0x00, }; -twin_pixmap_t *twin_make_cursor(int *hx, int *hy) +twin_pixmap_t *_twin_make_cursor(void) { twin_pointer_t pixels = {.v = (void *) _twin_cursor_default}; - - twin_pixmap_t *cur = - twin_pixmap_create_const(TWIN_ARGB32, 14, 20, 14 * 4, pixels); - if (!cur) - return NULL; - *hx = 0; - *hy = 0; - return cur; + return twin_pixmap_create_const(TWIN_ARGB32, 14, 20, 14 * 4, pixels); } diff --git a/src/dispatch.c b/src/dispatch.c deleted file mode 100644 index a534adac..00000000 --- a/src/dispatch.c +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * Copyright (c) 2024 National Cheng Kung University, Taiwan - * All rights reserved. - */ - -#include - -#include "twin_private.h" - -extern twin_backend_t g_twin_backend; - -/* Execute a single iteration of the event dispatch loop. - * - * This function processes one iteration of the event loop and is designed - * for integration with external event loops (e.g., browser, GUI toolkits). - * - * Processing order: - * 1. Run pending timeout callbacks - * 2. Execute queued work items - * 3. Poll backend for new events - * - * Platform behavior: - * - Native: Returns control immediately after one iteration - * - Emscripten/WASM: Must be called via emscripten_set_main_loop_arg() - * to integrate with browser's event loop (requestAnimationFrame) - * - * @ctx : The Twin context to dispatch events for (must not be NULL) - * Return false if backend requested termination (e.g., window closed), - * true otherwise to continue dispatching - * - * Example usage with Emscripten (REQUIRED for WASM backends): - * static void main_loop(void *arg) - * { - * if (!twin_dispatch_once((twin_context_t *) arg)) - * emscripten_cancel_main_loop(); - * } - * emscripten_set_main_loop_arg(main_loop, ctx, 0, 1); - * - * Example usage in native backends (alternative to twin_dispatch): - * while (twin_dispatch_once(ctx)) - * ; // Continue until termination requested - */ -bool twin_dispatch_once(twin_context_t *ctx) -{ - /* Validate context to prevent null pointer dereference in callbacks */ - if (!ctx) { - log_error("twin_dispatch_once: NULL context"); - return false; - } - - _twin_run_timeout(); - _twin_run_work(); - - /* Poll backend for events. Returns false when user requests exit. - * If no poll function is registered, assume no event source and yield CPU. - */ - if (g_twin_backend.poll) { - if (!g_twin_backend.poll(ctx)) - return false; - } else { - log_warn("twin_dispatch_once: No backend poll function registered"); - - /* CPU yielding strategy (platform-dependent): - * - * Emscripten/WASM builds: - * No explicit yielding needed. When this function returns, control - * goes back to the browser's event loop (set up via - * emscripten_set_main_loop_arg). The browser automatically handles - * scheduling via requestAnimationFrame(), ensuring smooth rendering - * without consuming 100% CPU. - * - * Native POSIX builds: - * Use usleep(1000) to yield CPU for 1ms, preventing busy-waiting - * when there's no event source. This is a fallback for backends - * that don't implement the poll() function properly. - */ -#if !defined(__EMSCRIPTEN__) && defined(_POSIX_VERSION) - usleep(1000); /* 1ms sleep to prevent busy-waiting */ -#endif - /* WASM: No action needed - browser handles scheduling */ - } - - return true; -} diff --git a/src/draw-builtin.c b/src/draw-builtin.c index 423af81f..1d285b62 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -48,70 +48,23 @@ * combinations of source/mask/destination format operations. */ -static inline twin_argb32_t in_over(twin_argb32_t dst, - twin_argb32_t src, - twin_a8_t msk) -{ - uint16_t t1, t2, t3, t4; - twin_a8_t a; - - switch (msk) { - case 0: - return dst; - case 0xff: - break; - default: - src = (twin_in(src, 0, msk, t1) | twin_in(src, 8, msk, t2) | - twin_in(src, 16, msk, t3) | twin_in(src, 24, msk, t4)); - break; - } - if (!src) - return dst; - a = ~(src >> 24); - switch (a) { - case 0: - return src; - case 0xff: - dst = (twin_add(src, dst, 0, t1) | twin_add(src, dst, 8, t2) | - twin_add(src, dst, 16, t3) | twin_add(src, dst, 24, t4)); - break; - default: - dst = (twin_over(src, dst, 0, a, t1) | twin_over(src, dst, 8, a, t2) | - twin_over(src, dst, 16, a, t3) | twin_over(src, dst, 24, a, t4)); - break; - } - return dst; -} - +/* Two channels per 32-bit multiply (see _twin_mul_lanes); every result is + * bit-identical to the per-channel twin_in/twin_over/twin_add forms */ static inline twin_argb32_t in(twin_argb32_t src, twin_a8_t msk) { - uint16_t t1, t2, t3, t4; - - return (twin_in(src, 0, msk, t1) | twin_in(src, 8, msk, t2) | - twin_in(src, 16, msk, t3) | twin_in(src, 24, msk, t4)); + return _twin_mul_lanes(src & TWIN_LANES, msk) | + _twin_mul_lanes((src >> 8) & TWIN_LANES, msk) << 8; } -static inline twin_argb32_t over(twin_argb32_t dst, twin_argb32_t src) +static inline twin_argb32_t in_over(twin_argb32_t dst, + twin_argb32_t src, + twin_a8_t msk) { - uint16_t t1, t2, t3, t4; - twin_a8_t a; - - if (!src) + if (!msk) return dst; - a = ~(src >> 24); - switch (a) { - case 0: - return src; - case 0xff: - dst = (twin_add(src, dst, 0, t1) | twin_add(src, dst, 8, t2) | - twin_add(src, dst, 16, t3) | twin_add(src, dst, 24, t4)); - break; - default: - dst = (twin_over(src, dst, 0, a, t1) | twin_over(src, dst, 8, a, t2) | - twin_over(src, dst, 16, a, t3) | twin_over(src, dst, 24, a, t4)); - break; - } - return dst; + if (msk != 0xff) + src = in(src, msk); + return _twin_over(dst, src); } static inline twin_argb32_t rgb16_to_argb32(twin_rgb16_t v) @@ -155,9 +108,6 @@ static inline twin_a8_t argb32_to_a8(twin_argb32_t v) #define msk_c (argb32_to_a8(msk.c)) #define msk_argb32 (argb32_to_a8(*msk.p.argb32++)) -#define msk_rgb16 \ - (0xff); \ - (void) msk #define msk_a8 (*msk.p.a8++) #define CAT2(a, b) a##b @@ -204,7 +154,6 @@ static inline twin_a8_t argb32_to_a8(twin_argb32_t v) /* clang-format off */ #define MAKE_TWIN_in_op_msks(op, dst, src) \ CAT2(MAKE_TWIN_in_, op)(dst, src, argb32) \ - CAT2(MAKE_TWIN_in_, op)(dst, src, rgb16) \ CAT2(MAKE_TWIN_in_, op)(dst, src, a8) \ CAT2(MAKE_TWIN_in_, op)(dst, src, c) /* clang-format on */ @@ -228,7 +177,7 @@ MAKE_TWIN_in_op_dsts_srcs_msks(over) MAKE_TWIN_in_op_dsts_srcs_msks(source) while (width--) { \ dst32 = CAT3(dst_, __dst, _get); \ src32 = CAT2(src_, __src); \ - dst32 = over(dst32, src32); \ + dst32 = _twin_over(dst32, src32); \ CAT3(dst_, __dst, _set)(dst32); \ } \ } @@ -327,25 +276,23 @@ static const twin_src_op comp2[2][4][3] = { }, }; +/* Mask index: RGB16 has no alpha and composites as no mask, so it never + * reaches the table */ +enum { MSK_A8, MSK_ARGB32, MSK_C }; + /* op, src, msk, dst */ -static const twin_src_msk_op comp3[2][4][4][3] = { +static const twin_src_msk_op comp3[2][4][3][3] = { [TWIN_OVER] = { [TWIN_A8] = { - [TWIN_A8] = + [MSK_A8] = { _twin_a8_in_a8_over_a8, _twin_a8_in_a8_over_rgb16, _twin_a8_in_a8_over_argb32, }, - [TWIN_RGB16] = - { - _twin_a8_in_rgb16_over_a8, - _twin_a8_in_rgb16_over_rgb16, - _twin_a8_in_rgb16_over_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_a8_in_argb32_over_a8, _twin_a8_in_argb32_over_rgb16, @@ -360,19 +307,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, [TWIN_RGB16] = { - [TWIN_A8] = + [MSK_A8] = { _twin_rgb16_in_a8_over_a8, _twin_rgb16_in_a8_over_rgb16, _twin_rgb16_in_a8_over_argb32, }, - [TWIN_RGB16] = - { - _twin_rgb16_in_rgb16_over_a8, - _twin_rgb16_in_rgb16_over_rgb16, - _twin_rgb16_in_rgb16_over_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_rgb16_in_argb32_over_a8, _twin_rgb16_in_argb32_over_rgb16, @@ -387,19 +328,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, [TWIN_ARGB32] = { - [TWIN_A8] = + [MSK_A8] = { _twin_argb32_in_a8_over_a8, _twin_argb32_in_a8_over_rgb16, _twin_argb32_in_a8_over_argb32, }, - [TWIN_RGB16] = - { - _twin_argb32_in_rgb16_over_a8, - _twin_argb32_in_rgb16_over_rgb16, - _twin_argb32_in_rgb16_over_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_argb32_in_argb32_over_a8, _twin_argb32_in_argb32_over_rgb16, @@ -414,19 +349,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, { /* C */ - [TWIN_A8] = + [MSK_A8] = { _twin_c_in_a8_over_a8, _twin_c_in_a8_over_rgb16, _twin_c_in_a8_over_argb32, }, - [TWIN_RGB16] = - { - _twin_c_in_rgb16_over_a8, - _twin_c_in_rgb16_over_rgb16, - _twin_c_in_rgb16_over_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_c_in_argb32_over_a8, _twin_c_in_argb32_over_rgb16, @@ -444,19 +373,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { { [TWIN_A8] = { - [TWIN_A8] = + [MSK_A8] = { _twin_a8_in_a8_source_a8, _twin_a8_in_a8_source_rgb16, _twin_a8_in_a8_source_argb32, }, - [TWIN_RGB16] = - { - _twin_a8_in_rgb16_source_a8, - _twin_a8_in_rgb16_source_rgb16, - _twin_a8_in_rgb16_source_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_a8_in_argb32_source_a8, _twin_a8_in_argb32_source_rgb16, @@ -471,19 +394,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, [TWIN_RGB16] = { - [TWIN_A8] = + [MSK_A8] = { _twin_rgb16_in_a8_source_a8, _twin_rgb16_in_a8_source_rgb16, _twin_rgb16_in_a8_source_argb32, }, - [TWIN_RGB16] = - { - _twin_rgb16_in_rgb16_source_a8, - _twin_rgb16_in_rgb16_source_rgb16, - _twin_rgb16_in_rgb16_source_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_rgb16_in_argb32_source_a8, _twin_rgb16_in_argb32_source_rgb16, @@ -498,19 +415,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, [TWIN_ARGB32] = { - [TWIN_A8] = + [MSK_A8] = { _twin_argb32_in_a8_source_a8, _twin_argb32_in_a8_source_rgb16, _twin_argb32_in_a8_source_argb32, }, - [TWIN_RGB16] = - { - _twin_argb32_in_rgb16_source_a8, - _twin_argb32_in_rgb16_source_rgb16, - _twin_argb32_in_rgb16_source_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_argb32_in_argb32_source_a8, _twin_argb32_in_argb32_source_rgb16, @@ -525,19 +436,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { }, { /* C */ - [TWIN_A8] = + [MSK_A8] = { _twin_c_in_a8_source_a8, _twin_c_in_a8_source_rgb16, _twin_c_in_a8_source_argb32, }, - [TWIN_RGB16] = - { - _twin_c_in_rgb16_source_a8, - _twin_c_in_rgb16_source_rgb16, - _twin_c_in_rgb16_source_argb32, - }, - [TWIN_ARGB32] = + [MSK_ARGB32] = { _twin_c_in_argb32_source_a8, _twin_c_in_argb32_source_rgb16, @@ -557,6 +462,13 @@ static const twin_src_msk_op comp3[2][4][4][3] = { #define operand_index(o) \ ((o)->source_kind == TWIN_SOLID ? 3 : o->u.pixmap->format) +static inline int mask_index(const twin_operand_t *o) +{ + if (o->source_kind == TWIN_SOLID) + return MSK_C; + return o->u.pixmap->format == TWIN_A8 ? MSK_A8 : MSK_ARGB32; +} + static void _twin_composite_simple(twin_pixmap_t *dst, twin_coord_t dst_x, twin_coord_t dst_y, @@ -630,7 +542,7 @@ static void _twin_composite_simple(twin_pixmap_t *dst, mdx = msk_x - left; mdy = msk_y - top; - twin_src_msk_op op = comp3[operator][operand_index(src)][operand_index(msk)][dst->format]; + twin_src_msk_op op = comp3[operator][operand_index(src)][mask_index(msk)][dst->format]; for (iy = top; iy < bottom; iy++) { if (src->source_kind == TWIN_PIXMAP) s.p = twin_pixmap_pointer(src->u.pixmap, left + sdx, iy + sdy); @@ -691,6 +603,7 @@ static twin_xform_t *twin_pixmap_init_xform(twin_pixmap_t *pixmap, xform->width = width; xform->src_x = src_x; xform->src_y = src_y; + xform->off_x = xform->off_y = 0; return xform; } @@ -722,13 +635,14 @@ static void twin_pixmap_free_xform(twin_xform_t *xform) : *((pix)->p.a8 + XF(y) * (pix)->stride + XF(x)); \ } while (0) -#define _get_pix_16(d, pix, x, y) \ - do { \ - twin_rgb16_t p = \ - _pix_clipped(pix, x, y) \ - ? 0 \ - : *((pix)->p.argb32 + XF(y) * ((pix)->stride >> 1) + XF(x)); \ - *((twin_argb32_t *) (char *) (d)) = twin_rgb16_to_argb32(p); \ +/* Outside the pixmap is transparent, as for the other formats */ +#define _get_pix_16(d, pix, x, y) \ + do { \ + *((twin_argb32_t *) (char *) (d)) = \ + _pix_clipped(pix, x, y) \ + ? 0 \ + : twin_rgb16_to_argb32(*( \ + (pix)->p.rgb16 + XF(y) * ((pix)->stride >> 1) + XF(x))); \ } while (0) #define _get_pix_32(d, pix, x, y) \ @@ -740,12 +654,17 @@ static void twin_pixmap_free_xform(twin_xform_t *xform) *((twin_argb32_t *) (char *) (d)) = p; \ } while (0) -#define _pix_saucemix(tl, tr, bl, br, wx, wy) \ - ((((((br * wx) + (bl * (TWIN_FIXED_ONE - wx))) >> 16) * wy) + \ - ((((tr * wx) + (tl * (TWIN_FIXED_ONE - wx))) >> 16) * \ - (TWIN_FIXED_ONE - wy))) >> \ +/* Linear blend of a and b at w (16.16): a * (1 - w) + b * w rounded down, + * written with one multiply */ +#define _pix_lerp(a, b, w) \ + ((((int32_t) (a) << 16) + \ + ((int32_t) (b) - (int32_t) (a)) * (int32_t) (w)) >> \ 16) +/* Bilinear blend of four samples */ +#define _pix_saucemix(tl, tr, bl, br, wx, wy) \ + _pix_lerp(_pix_lerp(tl, tr, wx), _pix_lerp(bl, br, wx), wy) + static void twin_pixmap_read_xform_8(twin_xform_t *xform, twin_coord_t line) { twin_fixed_t dx, dy, sx, sy; @@ -756,10 +675,16 @@ static void twin_pixmap_read_xform_8(twin_xform_t *xform, twin_coord_t line) twin_matrix_t *tfm = &xform->pixmap->transform; /* for each row in the dest line */ - dy = twin_int_to_fixed(line); - for (dx = 0; dx < twin_int_to_fixed(xform->width); dx += TWIN_FIXED_ONE) { - sx = _twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x); - sy = _twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y); + dy = twin_int_to_fixed(line + xform->off_y); + dx = twin_int_to_fixed(xform->off_x); + /* dx moves by whole pixels, so the source moves by exactly the first + * matrix column per pixel: no multiplies after the first */ + uint32_t ux = (uint32_t) (_twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x)); + uint32_t uy = (uint32_t) (_twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y)); + for (twin_coord_t i = 0; i < xform->width; + i++, ux += (uint32_t) tfm->m[0][0], uy += (uint32_t) tfm->m[0][1]) { + sx = (twin_fixed_t) ux; + sy = (twin_fixed_t) uy; _get_pix_8(pts[0], pix, sx, sy); _get_pix_8(pts[1], pix, sx + TWIN_FIXED_ONE, sy); _get_pix_8(pts[2], pix, sx, sy + TWIN_FIXED_ONE); @@ -778,16 +703,31 @@ static void twin_pixmap_read_xform_16(twin_xform_t *xform, twin_coord_t line) twin_a8_t *dst = xform->span.a8; twin_pixmap_t *pix = xform->pixmap; twin_matrix_t *tfm = &xform->pixmap->transform; + twin_coord_t last_x = 0, last_y = 0; + bool fetched = false; /* for each row in the dest line */ - dy = twin_int_to_fixed(line); - for (dx = 0; dx < twin_int_to_fixed(xform->width); dx += TWIN_FIXED_ONE) { - sx = _twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x); - sy = _twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y); - _get_pix_16(pts[0], pix, sx, sy); - _get_pix_16(pts[1], pix, sx + TWIN_FIXED_ONE, sy); - _get_pix_16(pts[2], pix, sx, sy + TWIN_FIXED_ONE); - _get_pix_16(pts[3], pix, sx + TWIN_FIXED_ONE, sy + TWIN_FIXED_ONE); + dy = twin_int_to_fixed(line + xform->off_y); + dx = twin_int_to_fixed(xform->off_x); + /* dx moves by whole pixels, so the source moves by exactly the first + * matrix column per pixel: no multiplies after the first */ + uint32_t ux = (uint32_t) (_twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x)); + uint32_t uy = (uint32_t) (_twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y)); + for (twin_coord_t i = 0; i < xform->width; + i++, ux += (uint32_t) tfm->m[0][0], uy += (uint32_t) tfm->m[0][1]) { + sx = (twin_fixed_t) ux; + sy = (twin_fixed_t) uy; + /* The four texels depend on the integer position only: scaling up + * revisits them, so fetch (and convert) them once per texel */ + if (!fetched || XF(sx) != last_x || XF(sy) != last_y) { + fetched = true; + last_x = XF(sx); + last_y = XF(sy); + _get_pix_16(pts[0], pix, sx, sy); + _get_pix_16(pts[1], pix, sx + TWIN_FIXED_ONE, sy); + _get_pix_16(pts[2], pix, sx, sy + TWIN_FIXED_ONE); + _get_pix_16(pts[3], pix, sx + TWIN_FIXED_ONE, sy + TWIN_FIXED_ONE); + } wx = sx & 0xffff; wy = sy & 0xffff; *(dst++) = @@ -809,16 +749,31 @@ static void twin_pixmap_read_xform_32(twin_xform_t *xform, twin_coord_t line) twin_a8_t *dst = xform->span.a8; twin_pixmap_t *pix = xform->pixmap; twin_matrix_t *tfm = &xform->pixmap->transform; + twin_coord_t last_x = 0, last_y = 0; + bool fetched = false; /* for each row in the dest line */ - dy = twin_int_to_fixed(line); - for (dx = 0; dx < twin_int_to_fixed(xform->width); dx += TWIN_FIXED_ONE) { - sx = _twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x); - sy = _twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y); - _get_pix_32(pts[0], pix, sx, sy); - _get_pix_32(pts[1], pix, sx + TWIN_FIXED_ONE, sy); - _get_pix_32(pts[2], pix, sx, sy + TWIN_FIXED_ONE); - _get_pix_32(pts[3], pix, sx + TWIN_FIXED_ONE, sy + TWIN_FIXED_ONE); + dy = twin_int_to_fixed(line + xform->off_y); + dx = twin_int_to_fixed(xform->off_x); + /* dx moves by whole pixels, so the source moves by exactly the first + * matrix column per pixel: no multiplies after the first */ + uint32_t ux = (uint32_t) (_twin_matrix_fx(tfm, dx, dy) + FX(xform->src_x)); + uint32_t uy = (uint32_t) (_twin_matrix_fy(tfm, dx, dy) + FX(xform->src_y)); + for (twin_coord_t i = 0; i < xform->width; + i++, ux += (uint32_t) tfm->m[0][0], uy += (uint32_t) tfm->m[0][1]) { + sx = (twin_fixed_t) ux; + sy = (twin_fixed_t) uy; + /* The four texels depend on the integer position only: scaling up + * revisits them, so fetch (and convert) them once per texel */ + if (!fetched || XF(sx) != last_x || XF(sy) != last_y) { + fetched = true; + last_x = XF(sx); + last_y = XF(sy); + _get_pix_32(pts[0], pix, sx, sy); + _get_pix_32(pts[1], pix, sx + TWIN_FIXED_ONE, sy); + _get_pix_32(pts[2], pix, sx, sy + TWIN_FIXED_ONE); + _get_pix_32(pts[3], pix, sx + TWIN_FIXED_ONE, sy + TWIN_FIXED_ONE); + } wx = sx & 0xffff; wy = sy & 0xffff; *(dst++) = @@ -889,6 +844,8 @@ static void _twin_composite_xform(twin_pixmap_t *dst, twin_pixmap_init_xform(src->u.pixmap, left, width, src_x, src_y); if (!sxform) return; + sxform->off_x = left - dst_x; + sxform->off_y = top - dst_y; s.p = sxform->span; } else { s.c = src->u.argb; @@ -904,12 +861,14 @@ static void _twin_composite_xform(twin_pixmap_t *dst, msk_y); if (!mxform) return; + mxform->off_x = left - dst_x; + mxform->off_y = top - dst_y; m.p = mxform->span; } else { m.c = msk->u.argb; } - twin_src_msk_op op = comp3[operator][operand_xindex(src)][operand_xindex(msk)] + twin_src_msk_op op = comp3[operator][operand_xindex(src)][mask_index(msk)] [dst->format]; for (twin_coord_t iy = top; iy < bottom; iy++) { if (src->source_kind == TWIN_PIXMAP) @@ -945,6 +904,11 @@ void twin_composite(twin_pixmap_t *dst, twin_coord_t width, twin_coord_t height) { + /* RGB16 carries no alpha, so as a mask it is fully opaque: no mask. + * Before the transform test, so neither path samples it. */ + if (msk && msk->source_kind == TWIN_PIXMAP && + msk->u.pixmap->format == TWIN_RGB16) + msk = NULL; if ((src->source_kind == TWIN_PIXMAP && !twin_matrix_is_identity(&src->u.pixmap->transform)) || (msk && (msk->source_kind == TWIN_PIXMAP && diff --git a/src/draw-common.c b/src/draw-common.c deleted file mode 100644 index 5c5a3d4b..00000000 --- a/src/draw-common.c +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan - * All rights reserved. - */ - -#include "shadow-gaussian-lut.h" -#include "twin_private.h" - -static void _twin_apply_stack_blur(twin_pixmap_t *trg_px, - twin_pixmap_t *src_px, - int radius, - int first_str, - int first_end, - int second_str, - int second_end, - bool horiz_scan) -{ - int den = (radius + 1) * (radius + 1); - twin_pointer_t src_ptr, trg_ptr, old_ptr, new_ptr; - twin_argb32_t sumInR, sumOutR, sumR, sumInG, sumOutG, sumG, sumInB, sumOutB, - sumB, sumInA, sumOutA, sumA, _cur, _old, _new, _src; - uint16_t tmpR, tmpG, tmpB, tmpA; - for (int first = first_str; first < first_end; first++) { - sumInR = sumOutR = sumR = sumInG = sumOutG = sumG = sumInB = sumOutB = - sumB = sumInA = sumOutA = sumA = 0x00000000; - - /* Initialize sumOut by padding. */ - if (horiz_scan) - src_ptr = twin_pixmap_pointer(src_px, second_str, first); - else - src_ptr = twin_pixmap_pointer(src_px, first, second_str); - _src = *src_ptr.argb32; - - for (int i = second_str; i < second_str + radius; i++) { - sumOutR = _twin_add_ARGB(sumOutR, _src, 0, tmpR); - sumOutG = _twin_add_ARGB(sumOutG, _src, 8, tmpG); - sumOutB = _twin_add_ARGB(sumOutB, _src, 16, tmpB); - sumOutA = _twin_add_ARGB(sumOutA, _src, 24, tmpA); - for (int j = 0; j < (i - second_str) + 1; j++) { - sumR = _twin_add_ARGB(sumR, _src, 0, tmpR); - sumG = _twin_add_ARGB(sumG, _src, 8, tmpG); - sumB = _twin_add_ARGB(sumB, _src, 16, tmpB); - sumA = _twin_add_ARGB(sumA, _src, 24, tmpA); - } - } - - /* Initialize sumIn. */ - for (int i = second_str; i < second_str + radius; i++) { - if (horiz_scan) - src_ptr = twin_pixmap_pointer(src_px, i, first); - else - src_ptr = twin_pixmap_pointer(src_px, first, i); - _src = *src_ptr.argb32; - sumInR = _twin_add_ARGB(sumInR, _src, 0, tmpR); - sumInG = _twin_add_ARGB(sumInG, _src, 8, tmpG); - sumInB = _twin_add_ARGB(sumInB, _src, 16, tmpB); - sumInA = _twin_add_ARGB(sumInA, _src, 24, tmpA); - for (int j = 0; j < radius - (i - second_str); j++) { - sumR = _twin_add_ARGB(sumR, _src, 0, tmpR); - sumG = _twin_add_ARGB(sumG, _src, 8, tmpG); - sumB = _twin_add_ARGB(sumB, _src, 16, tmpB); - sumA = _twin_add_ARGB(sumA, _src, 24, tmpA); - } - } - - for (int cur = second_str; cur < second_end; cur++) { - if (horiz_scan) { - src_ptr = twin_pixmap_pointer(src_px, cur, first); - trg_ptr = twin_pixmap_pointer(trg_px, cur, first); - old_ptr = twin_pixmap_pointer( - src_px, max(cur - radius, second_str), first); - new_ptr = twin_pixmap_pointer( - src_px, min(cur + radius, second_end - 1), first); - } else { - src_ptr = twin_pixmap_pointer(src_px, first, cur); - trg_ptr = twin_pixmap_pointer(trg_px, first, cur); - old_ptr = twin_pixmap_pointer(src_px, first, - max(cur - radius, second_str)); - new_ptr = twin_pixmap_pointer( - src_px, first, min(cur + radius, second_end - 1)); - } - _cur = *src_ptr.argb32; - _old = *old_ptr.argb32; - _new = *new_ptr.argb32; - /* STEP 1 : sumOut + current */ - sumOutR = _twin_add_ARGB(sumOutR, _cur, 0, tmpR); - sumOutG = _twin_add_ARGB(sumOutG, _cur, 8, tmpG); - sumOutB = _twin_add_ARGB(sumOutB, _cur, 16, tmpB); - sumOutA = _twin_add_ARGB(sumOutA, _cur, 24, tmpA); - /* STEP 2 : sumIn + new */ - sumInR = _twin_add_ARGB(sumInR, _new, 0, tmpR); - sumInG = _twin_add_ARGB(sumInG, _new, 8, tmpG); - sumInB = _twin_add_ARGB(sumInB, _new, 16, tmpB); - sumInA = _twin_add_ARGB(sumInA, _new, 24, tmpA); - /* STEP 3 : sum + sumIn */ - sumR = _twin_add(sumR, sumInR, tmpR); - sumG = _twin_add(sumG, sumInG, tmpG); - sumB = _twin_add(sumB, sumInB, tmpB); - sumA = _twin_add(sumA, sumInA, tmpA); - /* STEP 4 : sum / denominator */ - *trg_ptr.argb32 = - (_twin_div(sumR, den, 0, tmpR) | _twin_div(sumG, den, 8, tmpG) | - _twin_div(sumB, den, 16, tmpB) | - _twin_div(sumA, den, 24, tmpA)); - /* STEP 5 : sum - sumOut */ - sumR = _twin_sub(sumR, sumOutR, tmpR); - sumG = _twin_sub(sumG, sumOutG, tmpG); - sumB = _twin_sub(sumB, sumOutB, tmpB); - sumA = _twin_sub(sumA, sumOutA, tmpA); - /* STEP 6 : sumOut - old */ - sumOutR = _twin_sub_ARGB(sumOutR, _old, 0, tmpR); - sumOutG = _twin_sub_ARGB(sumOutG, _old, 8, tmpG); - sumOutB = _twin_sub_ARGB(sumOutB, _old, 16, tmpB); - sumOutA = _twin_sub_ARGB(sumOutA, _old, 24, tmpA); - /* STEP 7 : sumIn - current */ - sumInR = _twin_sub_ARGB(sumInR, _cur, 0, tmpR); - sumInG = _twin_sub_ARGB(sumInG, _cur, 8, tmpG); - sumInB = _twin_sub_ARGB(sumInB, _cur, 16, tmpB); - sumInA = _twin_sub_ARGB(sumInA, _cur, 24, tmpA); - } - } -} - -void twin_stack_blur(twin_pixmap_t *px, - int radius, - twin_coord_t left, - twin_coord_t right, - twin_coord_t top, - twin_coord_t bottom) -{ - if (px->format != TWIN_ARGB32) - return; - twin_pixmap_t *tmp_px = - twin_pixmap_create(px->format, px->width, px->height); - memcpy(tmp_px->p.v, px->p.v, - px->width * px->height * twin_bytes_per_pixel(px->format)); - /* - * Originally, performing a 2D convolution on each pixel takes O(width * - * height * k²). However, by first scanning horizontally and then vertically - * across the pixel map, and applying a 1D convolution to each pixel, the - * complexity is reduced to O(2 * width * height * k). - */ - /* Horizontally scan. */ - _twin_apply_stack_blur(tmp_px, px, radius, top, bottom, left, right, true); - /* Vertically scan. */ - _twin_apply_stack_blur(px, tmp_px, radius, left, right, top, bottom, false); - twin_pixmap_destroy(tmp_px); - return; -} - -#if defined(CONFIG_DROP_SHADOW) - -#define TWIN_TITLE_HEIGHT 20 - -/* Reuse bottom-strip weights between frames when width is unchanged. */ -static twin_a8_t *shadow_bottom_cache_weights; -static twin_coord_t shadow_bottom_cache_capacity; -static twin_coord_t shadow_bottom_cache_width; - -/* Remember last vertical alpha ramp keyed by offset+alpha. */ -static twin_a8_t shadow_alpha_y_cache[(CONFIG_VERTICAL_OFFSET > 0) - ? CONFIG_VERTICAL_OFFSET - : 1]; -static twin_coord_t shadow_alpha_y_cache_offset = -1; -static twin_a8_t shadow_alpha_y_cache_alpha; -static bool shadow_alpha_y_cache_valid; - -#define SHADOW_LUT_X_LEN TWIN_SHADOW_EXTENT_X -#define SHADOW_LUT_Y_LEN TWIN_SHADOW_EXTENT_Y - -/* Fast lookup: 17-entry approximation of (1 - t^2)^2 * 0.92 + 0.08. */ -static inline twin_fixed_t shadow_gaussian_weight(twin_fixed_t t) -{ - if (t <= 0) - return shadow_gaussian_lut[0]; - if (t >= TWIN_FIXED_ONE) - return shadow_gaussian_lut[16]; - - int32_t scaled = t; - int32_t index = - (int32_t) (((int64_t) scaled * 16 + 0x8000) >> 16); /* rounded */ - if (index < 0) - index = 0; - else if (index > 16) - index = 16; - return shadow_gaussian_lut[index]; -} - -/* Renders a shadow effect that emulates the CSS 'box-shadow' property. - * - * This function uses precomputed fixed-point lookup tables for performance, - * approximating a Gaussian blur to create a soft shadow effect. The shadow's - * appearance is controlled by several compile-time options: - * - CONFIG_HORIZONTAL_OFFSET : Simulates the 'offset-x' value in CSS, - * determining the horizontal displacement of the shadow. - * - CONFIG_VERTICAL_OFFSET : Simulates the 'offset-y' value, controlling the - * vertical displacement. - * - CONFIG_SHADOW_BLUR : Controls how long the Gaussian falloff extends after - * the configured offset. - * - CONFIG_SHADOW_FADE_TAIL : Enforces a minimum transparent tail so the mask - * still feathers out cleanly at the far edge. - * - * Limitations: - * - This implementation does not support the 'spread-radius' or 'inset' - * keywords from the CSS 'box-shadow' specification. The shadow is always an - * outer shadow, and its initial size is based on the window dimensions. - * - The shadow quality and performance are tied to the fixed-point precision - * and the size of the lookup table (i.e., 'shadow_gaussian_lut'). - */ -void twin_shadow_border(twin_pixmap_t *shadow, twin_argb32_t color) -{ - twin_coord_t win_width = shadow->width - shadow->window->shadow_x; - twin_coord_t win_height = shadow->height - shadow->window->shadow_y; - twin_coord_t y_start; - - twin_a8_t base_alpha = (color >> 24) & 0xFF; - twin_argb32_t base_rgb = color & 0x00FFFFFF; - const twin_coord_t x_offset = CONFIG_HORIZONTAL_OFFSET; - const twin_coord_t y_offset = CONFIG_VERTICAL_OFFSET; - const twin_coord_t fade_tail = TWIN_SHADOW_FADE_EXTENT; - - if (!base_alpha) - return; - - /* Title-style windows leave the frame untouched by the shadow strip. */ - switch (shadow->window->style) { - case TwinWindowApplication: - /* Start below the title bar and apply the configured vertical offset. - */ - y_start = TWIN_TITLE_HEIGHT + y_offset; - break; - case TwinWindowPlain: - default: - y_start = y_offset; - break; - } - - const twin_coord_t lut_x_len = x_offset + fade_tail; - const twin_coord_t lut_y_len = y_offset + fade_tail; - twin_coord_t right_extent = shadow->window->shadow_x; - twin_coord_t bottom_extent = shadow->window->shadow_y; - - /* Shadow mask rendering assumes ARGB32 pixel layout throughout. */ - if (shadow->format != TWIN_ARGB32) - return; - - if (right_extent <= 0 && bottom_extent <= 0) - return; - - /* Clear stale shadow pixels before repainting the mask. */ - if (right_extent > 0) { - for (twin_coord_t y = 0; y < win_height; y++) { - twin_pointer_t clear = twin_pixmap_pointer(shadow, win_width, y); - memset(clear.argb32, 0, - (size_t) right_extent * sizeof(*clear.argb32)); - } - } - if (bottom_extent > 0) { - for (twin_coord_t y = win_height; y < shadow->height; y++) { - twin_pointer_t clear = twin_pixmap_pointer(shadow, 0, y); - memset(clear.argb32, 0, - (size_t) shadow->width * sizeof(*clear.argb32)); - } - } - - /* Clamp rendering extent to the LUT dimensions. */ - if (right_extent > lut_x_len) - right_extent = lut_x_len; - if (bottom_extent > lut_y_len) - bottom_extent = lut_y_len; - -/* Fixed-point lookup tables for edge falloffs (small: <= 10 entries). */ -#if CONFIG_HORIZONTAL_OFFSET <= 0 || CONFIG_VERTICAL_OFFSET <= 0 -#error "Drop shadow offsets must be positive." -#endif - twin_a8_t alpha_lut_x[SHADOW_LUT_X_LEN]; /* Right-edge fade LUT. */ - twin_a8_t alpha_lut_y[SHADOW_LUT_Y_LEN]; /* Bottom fade LUT. */ - - twin_fixed_t step_x = - (x_offset > 1) ? (TWIN_FIXED_ONE / (x_offset - 1)) : 0; - twin_fixed_t tx = 0; - for (twin_coord_t i = 0; i < x_offset; i++) { - twin_fixed_t weight = shadow_gaussian_weight(tx); - alpha_lut_x[i] = (twin_a8_t) ((weight * base_alpha) >> 16); - if (x_offset > 1 && tx < TWIN_FIXED_ONE) - tx += step_x; - } - for (twin_coord_t i = x_offset; i < lut_x_len; i++) - alpha_lut_x[i] = 0; - - bool reuse_alpha_y = false; - if (shadow_alpha_y_cache_valid && shadow_alpha_y_cache_offset == y_offset && - shadow_alpha_y_cache_alpha == base_alpha) { - memcpy(alpha_lut_y, shadow_alpha_y_cache, - (size_t) y_offset * sizeof(*alpha_lut_y)); - reuse_alpha_y = true; - } - - if (!reuse_alpha_y) { - twin_fixed_t step_y = - (y_offset > 1) ? (TWIN_FIXED_ONE / (y_offset - 1)) : 0; - twin_fixed_t ty = 0; - for (twin_coord_t i = 0; i < y_offset; i++) { - twin_fixed_t weight = shadow_gaussian_weight(ty); - alpha_lut_y[i] = (twin_a8_t) ((weight * base_alpha) >> 16); - if (y_offset > 1 && ty < TWIN_FIXED_ONE) - ty += step_y; - } - if (y_offset > 0) { - memcpy(shadow_alpha_y_cache, alpha_lut_y, - (size_t) y_offset * sizeof(*alpha_lut_y)); - shadow_alpha_y_cache_offset = y_offset; - shadow_alpha_y_cache_alpha = base_alpha; - shadow_alpha_y_cache_valid = true; - } - } - for (twin_coord_t i = y_offset; i < lut_y_len; i++) - alpha_lut_y[i] = 0; - - /* Right edge strip */ - if (right_extent > 0 && y_start < shadow->height) { - twin_coord_t y_end = win_height; - if (y_end > shadow->height) - y_end = shadow->height; - for (twin_coord_t y = y_start; y < y_end; y++) { - twin_argb32_t *row = - (twin_argb32_t *) (shadow->p.b + y * shadow->stride); - twin_argb32_t *dst = row + win_width; - for (twin_coord_t i = 0; i < right_extent; i++) - dst[i] = (alpha_lut_x[i] << 24) | base_rgb; - } - } - - /* Bottom edge strip (extends to win_width; corner adjoins at win_width). */ - if (bottom_extent > 0) { - twin_coord_t bottom_start = win_height; - if (bottom_start < 0) - bottom_start = 0; - twin_coord_t bottom_end = win_height + bottom_extent; - if (bottom_end > shadow->height) - bottom_end = shadow->height; - - twin_coord_t left_skip = x_offset; - if (left_skip > win_width) - left_skip = win_width; - - twin_coord_t bottom_width = win_width - left_skip; - if (bottom_width < 0) - bottom_width = 0; - - twin_a8_t *alpha_bottom = NULL; - bool alpha_bottom_ready = false; - - if (bottom_width > 0) { - if (bottom_width > shadow_bottom_cache_capacity) { - twin_a8_t *newbuf = - twin_realloc(shadow_bottom_cache_weights, bottom_width); - if (newbuf) { - shadow_bottom_cache_weights = newbuf; - shadow_bottom_cache_capacity = bottom_width; - shadow_bottom_cache_width = 0; - } - } - if (shadow_bottom_cache_weights && - shadow_bottom_cache_capacity >= bottom_width) { - alpha_bottom = shadow_bottom_cache_weights; - if (shadow_bottom_cache_width == bottom_width) - alpha_bottom_ready = true; - } - if (!alpha_bottom) - alpha_bottom = twin_malloc(bottom_width); - - if (alpha_bottom && !alpha_bottom_ready) { - twin_fixed_t bottom_step = - (bottom_width > 1) ? (TWIN_FIXED_ONE / (bottom_width - 1)) - : 0; - twin_fixed_t u = 0; - for (twin_coord_t x = 0; x < bottom_width; x++) { - twin_fixed_t weight = shadow_gaussian_weight(u); - alpha_bottom[x] = (twin_a8_t) ((weight * 255) >> 16); - if (bottom_width > 1 && u < TWIN_FIXED_ONE) - u += bottom_step; - } - if (alpha_bottom == shadow_bottom_cache_weights) - shadow_bottom_cache_width = bottom_width; - } - } - - for (twin_coord_t y = bottom_start; y < bottom_end; y++) { - twin_argb32_t *row = - (twin_argb32_t *) (shadow->p.b + y * shadow->stride); - if (left_skip > 0) - memset(row, 0, (size_t) left_skip * sizeof(*row)); - - if (bottom_width <= 0) - continue; - - twin_coord_t dist_y = y - win_height; - twin_a8_t alpha_y = alpha_lut_y[dist_y]; - twin_argb32_t *dst = row + left_skip; - - if (alpha_bottom) { - for (twin_coord_t x = 0; x < bottom_width; x++) { - twin_a8_t alpha = (alpha_y * alpha_bottom[x]) >> 8; - dst[x] = (alpha << 24) | base_rgb; - } - } else { - twin_fixed_t bottom_step = - (bottom_width > 1) ? (TWIN_FIXED_ONE / (bottom_width - 1)) - : 0; - twin_fixed_t u = 0; - for (twin_coord_t x = 0; x < bottom_width; x++) { - twin_fixed_t weight = shadow_gaussian_weight(u); - twin_a8_t alpha_x = (twin_a8_t) ((weight * 255) >> 16); - twin_a8_t alpha = (alpha_y * alpha_x) >> 8; - dst[x] = (alpha << 24) | base_rgb; - if (bottom_width > 1 && u < TWIN_FIXED_ONE) - u += bottom_step; - } - } - } - - if (alpha_bottom && alpha_bottom != shadow_bottom_cache_weights) - twin_free(alpha_bottom); - } - - /* Bottom-right corner combines both falloffs. */ - if (right_extent > 0 && bottom_extent > 0) { - twin_coord_t corner_end = win_height + bottom_extent; - if (corner_end > shadow->height) - corner_end = shadow->height; - for (twin_coord_t y = win_height; y < corner_end; y++) { - twin_coord_t dist_y = y - win_height; - twin_a8_t alpha_y = alpha_lut_y[dist_y]; - twin_argb32_t *row = - (twin_argb32_t *) (shadow->p.b + y * shadow->stride); - twin_argb32_t *dst = row + win_width; - for (twin_coord_t i = 0; i < right_extent; i++) { - twin_a8_t alpha = (alpha_lut_x[i] * alpha_y) >> 8; - dst[i] = (alpha << 24) | base_rgb; - } - } - } -} - -#undef SHADOW_LUT_X_LEN -#undef SHADOW_LUT_Y_LEN -#endif - -static twin_argb32_t _twin_apply_alpha(twin_argb32_t v) -{ - uint16_t t1, t2, t3; - twin_a8_t alpha = twin_get_8(v, -#if __BYTE_ORDER == __BIG_ENDIAN - 0 -#else - 24 -#endif - ); - - /* clear RGB data if alpha is zero */ - if (!alpha) - return 0; - -#if __BYTE_ORDER == __BIG_ENDIAN - /* twin needs ARGB format */ - return 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 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 -} - -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]); - } -} - -void twin_cover(twin_pixmap_t *dst, - twin_argb32_t color, - twin_coord_t x, - twin_coord_t y, - twin_coord_t width) -{ - if (x < 0 || y < 0 || width < 0 || x + width > dst->width || - y >= dst->height) - return; - for (twin_coord_t i = 0; i < width; i++) { - twin_pointer_t pt = twin_pixmap_pointer(dst, x + i, y); - *pt.argb32 = color; - } -} diff --git a/src/draw-pixman.c b/src/draw-pixman.c index e849b2de..913bd039 100644 --- a/src/draw-pixman.c +++ b/src/draw-pixman.c @@ -50,14 +50,17 @@ static pixman_op_t twin_to_pixman_op(const twin_operator_t twin_op) (_pixmap)->p.argb32, (_pixmap)->stride); \ }) -static void pixmap_matrix_scale(pixman_image_t *src, twin_matrix_t *matrix) +/* Twin and pixman both map destination to source with 16.16 fixed point: + * src.x = m00 * x + m10 * y + m20, src.y = m01 * x + m11 * y + m21. + * Carry the whole affine matrix, translation included. */ +static void pixmap_matrix_transform(pixman_image_t *src, + const twin_matrix_t *matrix) { - pixman_transform_t transform; - pixman_transform_init_identity(&transform); - - pixman_fixed_t sx = matrix->m[0][0], sy = matrix->m[1][1]; - - pixman_transform_scale(&transform, NULL, sx, sy); + pixman_transform_t transform = {{ + {matrix->m[0][0], matrix->m[1][0], matrix->m[2][0]}, + {matrix->m[0][1], matrix->m[1][1], matrix->m[2][1]}, + {0, 0, pixman_fixed_1}, + }}; pixman_image_set_transform(src, &transform); } @@ -84,7 +87,7 @@ void twin_composite(twin_pixmap_t *_dst, src = create_pixman_image_from_twin_pixmap(src_pixmap); if (!twin_matrix_is_identity(&(src_pixmap->transform))) - pixmap_matrix_scale(src, &(src_pixmap->transform)); + pixmap_matrix_transform(src, &(src_pixmap->transform)); } pixman_image_t *dst = create_pixman_image_from_twin_pixmap(_dst); @@ -108,8 +111,11 @@ void twin_composite(twin_pixmap_t *_dst, if (oy + height > _dst->clip.bottom) height = _dst->clip.bottom - oy; - if (width < 0 || height < 0) + if (width <= 0 || height <= 0) { + pixman_image_unref(src); + pixman_image_unref(dst); return; + } if (!_msk) { pixman_image_composite(twin_to_pixman_op(operator), src, NULL, dst, diff --git a/src/fixed.c b/src/fixed.c index b76c2aad..aa3ccb7f 100644 --- a/src/fixed.c +++ b/src/fixed.c @@ -55,34 +55,6 @@ twin_fixed_t twin_fixed_sqrt(twin_fixed_t a) return (offset >= 0) ? z >> offset : z << (-offset); } -twin_sfixed_t _twin_sfixed_sqrt(twin_sfixed_t as) -{ - if (as <= 0) - return 0; - if (CHECK_INTERVAL(as, TWIN_SFIXED_ONE, (1 << (2 - 1)))) - return TWIN_SFIXED_ONE; - - int offset = 0; - for (twin_sfixed_t i = as; !(0x4000 & i); i <<= 1) - ++offset; - - offset &= ~1; - as <<= offset; - - offset >>= 1; - offset -= (4 >> 1); - - twin_sfixed_t z = 0; - for (twin_sfixed_t m = 1UL << ((31 - twin_clz(as)) & ~1UL); m; m >>= 2) { - int16_t b = z + m; - z >>= 1; - if (as >= b) - as -= b, z += m; - } - - return (offset >= 0) ? z >> offset : z << (-offset); -} - twin_xfixed_t _twin_xfixed_sqrt(twin_xfixed_t a) { /* Early return for non-positive inputs */ @@ -110,8 +82,7 @@ twin_xfixed_t _twin_xfixed_sqrt(twin_xfixed_t a) /* Digit-by-digit square root calculation */ twin_xfixed_t z = 0; - for (twin_xfixed_t m = 1ULL << ((63 - __builtin_clzll(a)) & ~1ULL); m; - m >>= 2) { + for (twin_xfixed_t m = 1ULL << ((63 - twin_clzll(a)) & ~1ULL); m; m >>= 2) { twin_xfixed_t b = z + m; z >>= 1; if (a >= b) diff --git a/src/font-icons.c b/src/font-icons.c new file mode 100644 index 00000000..d38b0716 --- /dev/null +++ b/src/font-icons.c @@ -0,0 +1,1764 @@ +/* + * Generated by tools/ttf/twin-ttf from Font Awesome 5 Free Solid. + * Do not edit: regenerate from the font instead. + */ + +#include "twin_private.h" + +/* clang-format off */ +static const signed char outlines[] = { + 0, + /* 0xf001 () */ 64, + 'm', 64, 52, + 'l', 64, 8, + '2', 64, 5, 61, 2, + '2', 57, 0, 52, 0, + '2', 47, 0, 44, 2, + '2', 40, 5, 40, 8, + '2', 40, 11, 44, 14, + '2', 47, 16, 52, 16, + '2', 54, 16, 56, 16, + 'l', 56, 39, + 'l', 24, 29, + 'l', 24, 0, + '2', 24, -3, 21, -6, + '2', 17, -8, 12, -8, + '2', 7, -8, 4, -6, + '2', 0, -3, 0, 0, + '2', 0, 3, 4, 6, + '2', 7, 8, 12, 8, + '2', 14, 8, 16, 8, + 'l', 16, 40, + '2', 16, 43, 19, 44, + 'l', 59, 56, + '2', 61, 56, 62, 55, + '2', 64, 54, 64, 52, + 'e', + /* 0xf007 () */ 56, + 'm', 39, 29, + '2', 35, 24, 28, 24, + '2', 21, 24, 17, 29, + '2', 12, 33, 12, 40, + '2', 12, 47, 17, 51, + '2', 21, 56, 28, 56, + '2', 35, 56, 39, 51, + '2', 44, 47, 44, 40, + '2', 44, 33, 39, 29, + 'm', 39, 20, + '2', 46, 20, 51, 15, + '2', 56, 10, 56, 3, + 'l', 56, -2, + '2', 56, -4, 54, -6, + '2', 53, -8, 50, -8, + 'l', 6, -8, + '2', 4, -8, 2, -6, + '2', 0, -4, 0, -2, + 'l', 0, 3, + '2', 0, 10, 5, 15, + '2', 10, 20, 17, 20, + 'l', 19, 20, + '2', 23, 18, 28, 18, + '2', 33, 18, 37, 20, + 'l', 39, 20, + 'e', + /* 0xf00c () */ 64, + 'm', 22, 1, + 'l', 1, 22, + '2', 0, 23, 0, 24, + '2', 0, 26, 1, 26, + 'l', 6, 31, + '2', 6, 32, 8, 32, + '2', 9, 32, 10, 31, + 'l', 24, 17, + 'l', 54, 47, + '2', 55, 48, 56, 48, + '2', 58, 48, 59, 47, + 'l', 63, 42, + '2', 64, 42, 64, 40, + '2', 64, 39, 63, 38, + 'l', 26, 1, + '2', 25, 0, 24, 0, + '2', 23, 0, 22, 1, + 'e', + /* 0xf012 () */ 80, + 'm', 27, 20, + '2', 28, 20, 28, 19, + '2', 29, 19, 29, 18, + 'l', 29, -6, + '2', 29, -7, 28, -7, + '2', 28, -8, 27, -8, + 'l', 21, -8, + '2', 20, -8, 20, -7, + '2', 19, -7, 19, -6, + 'l', 19, 18, + '2', 19, 19, 20, 19, + '2', 20, 20, 21, 20, + 'l', 27, 20, + 'm', 11, 8, + '2', 12, 8, 12, 7, + '2', 13, 7, 13, 6, + 'l', 13, -6, + '2', 13, -7, 12, -7, + '2', 12, -8, 11, -8, + 'l', 5, -8, + '2', 4, -8, 4, -7, + '2', 3, -7, 3, -6, + 'l', 3, 6, + '2', 3, 7, 4, 7, + '2', 4, 8, 5, 8, + 'l', 11, 8, + 'm', 43, 32, + '2', 44, 32, 44, 31, + '2', 45, 31, 45, 30, + 'l', 45, -6, + '2', 45, -7, 44, -7, + '2', 44, -8, 43, -8, + 'l', 37, -8, + '2', 36, -8, 36, -7, + '2', 35, -7, 35, -6, + 'l', 35, 30, + '2', 35, 31, 36, 31, + '2', 36, 32, 37, 32, + 'l', 43, 32, + 'm', 59, 44, + '2', 60, 44, 60, 43, + '2', 61, 43, 61, 42, + 'l', 61, -6, + '2', 61, -7, 60, -7, + '2', 60, -8, 59, -8, + 'l', 53, -8, + '2', 52, -8, 52, -7, + '2', 51, -7, 51, -6, + 'l', 51, 42, + '2', 51, 43, 52, 43, + '2', 52, 44, 53, 44, + 'l', 59, 44, + 'm', 75, 56, + '2', 76, 56, 76, 55, + '2', 77, 55, 77, 54, + 'l', 77, -6, + '2', 77, -7, 76, -7, + '2', 76, -8, 75, -8, + 'l', 69, -8, + '2', 68, -8, 68, -7, + '2', 67, -7, 67, -6, + 'l', 67, 54, + '2', 67, 55, 68, 55, + '2', 68, 56, 69, 56, + 'l', 75, 56, + 'e', + /* 0xf013 () */ 64, + 'm', 61, 17, + '2', 62, 16, 62, 15, + '2', 60, 8, 55, 3, + '2', 54, 2, 53, 3, + 'l', 48, 6, + '2', 44, 3, 40, 1, + 'l', 40, -5, + '2', 40, -6, 39, -6, + '2', 32, -8, 25, -6, + '2', 24, -6, 24, -5, + 'l', 24, 1, + '2', 20, 3, 16, 6, + 'l', 11, 3, + '2', 10, 2, 9, 3, + '2', 5, 8, 2, 15, + '2', 2, 16, 3, 17, + 'l', 8, 20, + '2', 8, 22, 8, 24, + '2', 8, 26, 8, 28, + 'l', 3, 32, + '2', 2, 32, 2, 33, + '2', 5, 40, 9, 45, + '2', 10, 46, 11, 45, + 'l', 16, 42, + '2', 20, 45, 24, 47, + 'l', 24, 53, + '2', 24, 54, 25, 54, + '2', 32, 56, 39, 54, + '2', 40, 54, 40, 53, + 'l', 40, 47, + '2', 44, 45, 48, 42, + 'l', 53, 45, + '2', 54, 46, 55, 45, + '2', 60, 40, 62, 33, + '2', 62, 32, 61, 32, + 'l', 56, 28, + '2', 56, 24, 56, 20, + 'l', 61, 17, + 'm', 25, 17, + '2', 28, 14, 32, 14, + '2', 36, 14, 39, 17, + '2', 42, 20, 42, 24, + '2', 42, 28, 39, 31, + '2', 36, 34, 32, 34, + '2', 28, 34, 25, 31, + '2', 22, 28, 22, 24, + '2', 22, 20, 25, 17, + 'e', + /* 0xf015 () */ 72, + 'm', 35, 38, + '2', 36, 38, 36, 38, + '2', 37, 38, 37, 38, + 'l', 60, 19, + 'l', 60, -2, + '2', 60, -3, 59, -3, + '2', 59, -4, 58, -4, + 'l', 44, -4, + '2', 43, -4, 43, -3, + '2', 42, -3, 42, -2, + 'l', 42, 10, + '2', 42, 11, 42, 11, + '2', 42, 12, 41, 12, + '2', 41, 12, 40, 12, + 'l', 32, 12, + '2', 31, 12, 31, 11, + '2', 30, 11, 30, 10, + 'l', 30, -2, + '2', 30, -3, 29, -3, + '2', 29, -4, 28, -4, + 'l', 14, -4, + '2', 13, -4, 13, -3, + '2', 12, -3, 12, -2, + 'l', 12, 19, + 'l', 35, 38, + 'm', 72, 25, + '2', 72, 24, 72, 23, + '2', 72, 23, 72, 23, + 'l', 69, 19, + '2', 68, 18, 67, 18, + '2', 67, 18, 66, 18, + 'l', 37, 43, + '2', 37, 43, 36, 43, + '2', 36, 43, 35, 43, + 'l', 6, 18, + '2', 5, 18, 5, 18, + '2', 4, 18, 4, 19, + 'l', 0, 23, + '2', 0, 23, 0, 23, + '2', 0, 24, 1, 25, + 'l', 32, 51, + '2', 34, 52, 36, 52, + '2', 38, 52, 40, 51, + 'l', 51, 41, + 'l', 51, 51, + '2', 51, 51, 51, 52, + '2', 52, 52, 53, 52, + 'l', 60, 52, + '2', 60, 52, 61, 52, + '2', 61, 51, 61, 51, + 'l', 61, 33, + 'l', 72, 25, + 'e', + /* 0xf017 () */ 64, + 'm', 10, 46, + '2', 19, 55, 32, 55, + '2', 45, 55, 54, 46, + '2', 63, 37, 63, 24, + '2', 63, 11, 54, 2, + '2', 45, -7, 32, -7, + '2', 19, -7, 10, 2, + '2', 1, 11, 1, 24, + '2', 1, 37, 10, 46, + 'm', 39, 11, + '2', 40, 10, 41, 12, + 'l', 45, 16, + '2', 46, 18, 45, 19, + 'l', 37, 24, + 'l', 37, 42, + '2', 37, 43, 35, 43, + 'l', 29, 43, + '2', 28, 43, 28, 42, + 'l', 28, 21, + '2', 28, 20, 28, 19, + 'l', 39, 11, + 'e', + /* 0xf01e () */ 64, + 'm', 63, 56, + '2', 63, 56, 64, 56, + '2', 64, 55, 64, 55, + 'l', 64, 30, + '2', 64, 29, 64, 28, + '2', 63, 28, 63, 28, + 'l', 38, 28, + '2', 37, 28, 36, 28, + '2', 36, 29, 36, 30, + 'l', 36, 35, + '2', 36, 36, 36, 36, + '2', 37, 37, 38, 37, + '2', 38, 37, 38, 37, + 'l', 50, 36, + '2', 48, 40, 42, 43, + '2', 37, 46, 32, 46, + '2', 23, 46, 16, 40, + '2', 10, 33, 10, 24, + '2', 10, 15, 16, 8, + '2', 23, 2, 32, 2, + '2', 36, 2, 40, 4, + '2', 44, 5, 47, 8, + '2', 47, 8, 48, 8, + '2', 48, 8, 49, 7, + 'l', 53, 3, + '2', 53, 3, 53, 2, + '2', 53, 1, 53, 1, + '2', 51, -1, 48, -2, + '2', 46, -4, 43, -5, + '2', 40, -6, 37, -6, + '2', 35, -7, 32, -7, + '2', 24, -7, 16, -3, + '2', 9, 1, 5, 8, + '2', 1, 16, 1, 24, + '2', 1, 37, 10, 46, + '2', 19, 55, 32, 55, + '2', 36, 55, 40, 54, + '2', 45, 52, 49, 50, + '2', 53, 47, 56, 44, + 'l', 55, 54, + '2', 55, 55, 55, 55, + '2', 55, 55, 56, 56, + '2', 56, 56, 57, 56, + 'l', 63, 56, + 'e', + /* 0xf021 () */ 64, + 'm', 55, 54, + '2', 55, 55, 55, 55, + '2', 55, 55, 56, 56, + '2', 56, 56, 57, 56, + 'l', 63, 56, + '2', 64, 56, 64, 55, + 'l', 64, 30, + '2', 64, 29, 64, 28, + '2', 63, 28, 63, 28, + 'l', 38, 28, + '2', 37, 28, 36, 28, + '2', 36, 29, 36, 30, + 'l', 36, 35, + '2', 36, 36, 36, 36, + '2', 37, 37, 38, 37, + 'l', 50, 36, + '2', 48, 40, 42, 43, + '2', 37, 46, 32, 46, + '2', 25, 46, 19, 41, + '2', 12, 36, 11, 29, + '2', 10, 28, 9, 28, + 'l', 3, 28, + '2', 2, 28, 2, 28, + '2', 2, 29, 2, 30, + '2', 2, 30, 2, 30, + '2', 4, 41, 12, 48, + '2', 21, 55, 32, 55, + '2', 36, 55, 40, 54, + '2', 45, 52, 49, 50, + '2', 53, 47, 56, 44, + 'l', 55, 54, + 'm', 32, 2, + '2', 39, 2, 45, 7, + '2', 52, 12, 53, 19, + '2', 54, 20, 55, 20, + 'l', 61, 20, + '2', 62, 20, 62, 20, + '2', 63, 19, 63, 19, + '2', 63, 18, 62, 18, + '2', 60, 7, 52, 0, + '2', 43, -7, 32, -7, + '2', 26, -7, 19, -4, + '2', 12, -1, 8, 4, + 'l', 9, -6, + '2', 9, -6, 9, -6, + '2', 9, -7, 8, -7, + '2', 8, -8, 7, -8, + 'l', 2, -8, + '2', 0, -8, 0, -6, + 'l', 0, 19, + '2', 0, 19, 0, 20, + '2', 1, 20, 2, 20, + 'l', 27, 20, + '2', 27, 20, 28, 20, + '2', 28, 19, 28, 19, + 'l', 28, 13, + '2', 28, 12, 28, 12, + '2', 27, 11, 27, 11, + 'l', 14, 12, + '2', 16, 8, 22, 5, + '2', 27, 2, 32, 2, + 'e', + /* 0xf028 () */ 72, + 'm', 27, 47, + '2', 28, 49, 30, 48, + '2', 32, 47, 32, 45, + 'l', 32, 3, + '2', 32, 1, 30, 0, + '2', 28, 0, 27, 1, + 'l', 16, 12, + 'l', 3, 12, + '2', 2, 12, 1, 13, + '2', 0, 14, 0, 15, + 'l', 0, 33, + '2', 0, 34, 1, 35, + '2', 2, 36, 3, 36, + 'l', 16, 36, + 'l', 27, 47, + 'm', 56, 54, + '2', 64, 49, 68, 41, + '2', 72, 33, 72, 24, + '2', 72, 15, 68, 7, + '2', 64, -1, 56, -5, + '2', 55, -6, 54, -6, + '2', 53, -6, 52, -5, + '2', 51, -4, 51, -2, + '2', 52, -1, 53, 0, + '2', 57, 2, 60, 6, + '2', 63, 10, 64, 15, + '2', 66, 19, 66, 24, + '2', 66, 31, 62, 38, + '2', 59, 44, 53, 49, + '2', 52, 49, 51, 50, + '2', 51, 52, 52, 53, + '2', 53, 54, 54, 54, + '2', 55, 54, 56, 54, + 'm', 57, 35, + '2', 60, 30, 60, 24, + '2', 60, 18, 57, 13, + '2', 54, 8, 49, 5, + '2', 48, 4, 47, 4, + '2', 46, 4, 45, 5, + '2', 45, 7, 45, 8, + '2', 45, 9, 46, 10, + '2', 49, 11, 50, 13, + '2', 52, 16, 53, 18, + '2', 54, 21, 54, 24, + '2', 54, 27, 53, 30, + '2', 52, 32, 50, 35, + '2', 49, 37, 46, 38, + '2', 45, 39, 45, 40, + '2', 45, 42, 45, 43, + '2', 46, 44, 47, 44, + '2', 48, 44, 49, 44, + '2', 54, 40, 57, 35, + 'm', 42, 34, + '2', 45, 32, 46, 30, + '2', 48, 27, 48, 24, + '2', 48, 21, 46, 18, + '2', 45, 16, 42, 14, + '2', 41, 14, 40, 14, + '2', 39, 15, 38, 16, + '2', 38, 17, 38, 18, + '2', 38, 19, 39, 20, + '2', 42, 21, 42, 24, + '2', 42, 27, 39, 28, + '2', 38, 29, 38, 30, + '2', 38, 31, 38, 32, + '2', 39, 34, 40, 34, + '2', 41, 34, 42, 34, + 'e', + /* 0xf030 () */ 64, + 'm', 64, 38, + 'l', 64, 2, + '2', 64, 0, 62, -2, + '2', 61, -4, 58, -4, + 'l', 6, -4, + '2', 4, -4, 2, -2, + '2', 0, 0, 0, 2, + 'l', 0, 38, + '2', 0, 41, 2, 42, + '2', 4, 44, 6, 44, + 'l', 17, 44, + 'l', 19, 48, + '2', 19, 49, 20, 50, + '2', 21, 51, 22, 52, + '2', 23, 52, 24, 52, + 'l', 40, 52, + '2', 42, 52, 43, 51, + '2', 45, 50, 46, 48, + 'l', 47, 44, + 'l', 58, 44, + '2', 61, 44, 62, 42, + '2', 64, 41, 64, 38, + 'm', 43, 9, + '2', 47, 14, 47, 20, + '2', 47, 26, 43, 31, + '2', 38, 35, 32, 35, + '2', 26, 35, 21, 31, + '2', 17, 26, 17, 20, + '2', 17, 14, 21, 9, + '2', 26, 5, 32, 5, + '2', 38, 5, 43, 9, + 'm', 40, 28, + '2', 43, 25, 43, 20, + '2', 43, 16, 40, 12, + '2', 37, 9, 32, 9, + '2', 28, 9, 24, 12, + '2', 21, 16, 21, 20, + '2', 21, 25, 24, 28, + '2', 28, 31, 32, 31, + '2', 37, 31, 40, 28, + 'e', + /* 0xf03a () */ 64, + 'm', 10, 10, + '2', 11, 10, 11, 9, + '2', 12, 9, 12, 8, + 'l', 12, 0, + '2', 12, -1, 11, -1, + '2', 11, -2, 10, -2, + 'l', 2, -2, + '2', 1, -2, 1, -1, + '2', 0, -1, 0, 0, + 'l', 0, 8, + '2', 0, 9, 1, 9, + '2', 1, 10, 2, 10, + 'l', 10, 10, + 'm', 10, 50, + '2', 11, 50, 11, 49, + '2', 12, 49, 12, 48, + 'l', 12, 40, + '2', 12, 39, 11, 39, + '2', 11, 38, 10, 38, + 'l', 2, 38, + '2', 1, 38, 1, 39, + '2', 0, 39, 0, 40, + 'l', 0, 48, + '2', 0, 49, 1, 49, + '2', 1, 50, 2, 50, + 'l', 10, 50, + 'm', 10, 30, + '2', 11, 30, 11, 29, + '2', 12, 29, 12, 28, + 'l', 12, 20, + '2', 12, 19, 11, 19, + '2', 11, 18, 10, 18, + 'l', 2, 18, + '2', 1, 18, 1, 19, + '2', 0, 19, 0, 20, + 'l', 0, 28, + '2', 0, 29, 1, 29, + '2', 1, 30, 2, 30, + 'l', 10, 30, + 'm', 62, 8, + '2', 63, 8, 63, 7, + '2', 64, 7, 64, 6, + 'l', 64, 2, + '2', 64, 1, 63, 1, + '2', 63, 0, 62, 0, + 'l', 22, 0, + '2', 21, 0, 21, 1, + '2', 20, 1, 20, 2, + 'l', 20, 6, + '2', 20, 7, 21, 7, + '2', 21, 8, 22, 8, + 'l', 62, 8, + 'm', 62, 48, + '2', 63, 48, 63, 47, + '2', 64, 47, 64, 46, + 'l', 64, 42, + '2', 64, 41, 63, 41, + '2', 63, 40, 62, 40, + 'l', 22, 40, + '2', 21, 40, 21, 41, + '2', 20, 41, 20, 42, + 'l', 20, 46, + '2', 20, 47, 21, 47, + '2', 21, 48, 22, 48, + 'l', 62, 48, + 'm', 62, 28, + '2', 63, 28, 63, 27, + '2', 64, 27, 64, 26, + 'l', 64, 22, + '2', 64, 21, 63, 21, + '2', 63, 20, 62, 20, + 'l', 22, 20, + '2', 21, 20, 21, 21, + '2', 20, 21, 20, 22, + 'l', 20, 26, + '2', 20, 27, 21, 27, + '2', 21, 28, 22, 28, + 'l', 62, 28, + 'e', + /* 0xf03e () */ 64, + 'm', 58, 0, + 'l', 6, 0, + '2', 4, 0, 2, 2, + '2', 0, 4, 0, 6, + 'l', 0, 42, + '2', 0, 45, 2, 46, + '2', 4, 48, 6, 48, + 'l', 58, 48, + '2', 61, 48, 62, 46, + '2', 64, 45, 64, 42, + 'l', 64, 6, + '2', 64, 4, 62, 2, + '2', 61, 0, 58, 0, + 'm', 19, 39, + '2', 17, 41, 14, 41, + '2', 11, 41, 9, 39, + '2', 7, 37, 7, 34, + '2', 7, 31, 9, 29, + '2', 11, 27, 14, 27, + '2', 17, 27, 19, 29, + '2', 21, 31, 21, 34, + '2', 21, 37, 19, 39, + 'm', 8, 8, + 'l', 56, 8, + 'l', 56, 22, + 'l', 45, 33, + '2', 44, 34, 43, 33, + 'l', 26, 16, + 'l', 19, 23, + '2', 18, 24, 17, 23, + 'l', 8, 14, + 'l', 8, 8, + 'e', + /* 0xf048 () */ 56, + 'm', 8, -2, + 'l', 8, 51, + '2', 8, 52, 10, 52, + 'l', 16, 52, + '2', 17, 52, 17, 51, + 'l', 17, 29, + 'l', 42, 51, + '2', 43, 53, 46, 52, + '2', 48, 51, 48, 48, + 'l', 48, 0, + '2', 48, -2, 46, -4, + '2', 43, -5, 42, -3, + 'l', 17, 19, + 'l', 17, -2, + '2', 17, -4, 16, -4, + 'l', 10, -4, + '2', 8, -4, 8, -2, + 'e', + /* 0xf04b () */ 56, + 'm', 53, 29, + '2', 56, 27, 56, 24, + '2', 56, 21, 53, 19, + 'l', 9, -7, + '2', 6, -9, 3, -7, + '2', 0, -5, 0, -2, + 'l', 0, 50, + '2', 0, 54, 3, 55, + '2', 6, 57, 9, 55, + 'l', 53, 29, + 'e', + /* 0xf04c () */ 56, + 'm', 18, -4, + 'l', 6, -4, + '2', 4, -4, 2, -2, + '2', 0, 0, 0, 2, + 'l', 0, 46, + '2', 0, 49, 2, 50, + '2', 4, 52, 6, 52, + 'l', 18, 52, + '2', 21, 52, 22, 50, + '2', 24, 49, 24, 46, + 'l', 24, 2, + '2', 24, 0, 22, -2, + '2', 21, -4, 18, -4, + 'm', 56, 2, + '2', 56, 0, 54, -2, + '2', 53, -4, 50, -4, + 'l', 38, -4, + '2', 36, -4, 34, -2, + '2', 32, 0, 32, 2, + 'l', 32, 46, + '2', 32, 49, 34, 50, + '2', 36, 52, 38, 52, + 'l', 50, 52, + '2', 53, 52, 54, 50, + '2', 56, 49, 56, 46, + 'l', 56, 2, + 'e', + /* 0xf051 () */ 56, + 'm', 48, 51, + 'l', 48, -2, + '2', 48, -4, 47, -4, + 'l', 41, -4, + '2', 39, -4, 39, -2, + 'l', 39, 20, + 'l', 15, -3, + '2', 13, -5, 10, -4, + '2', 8, -2, 8, 0, + 'l', 8, 48, + '2', 8, 51, 10, 52, + '2', 13, 53, 15, 51, + 'l', 39, 29, + 'l', 39, 51, + '2', 39, 52, 41, 52, + 'l', 47, 52, + '2', 48, 52, 48, 51, + 'e', + /* 0xf053 () */ 40, + 'm', 4, 26, + 'l', 29, 50, + '2', 30, 51, 31, 51, + '2', 32, 51, 33, 50, + 'l', 36, 48, + '2', 37, 47, 37, 46, + '2', 37, 44, 36, 43, + 'l', 16, 24, + 'l', 36, 5, + '2', 37, 4, 37, 3, + '2', 37, 1, 36, 0, + 'l', 33, -2, + '2', 32, -3, 31, -3, + '2', 30, -3, 29, -2, + 'l', 4, 22, + '2', 3, 23, 3, 24, + '2', 3, 25, 4, 26, + 'e', + /* 0xf054 () */ 40, + 'm', 36, 22, + 'l', 11, -2, + '2', 11, -3, 9, -3, + '2', 8, -3, 7, -2, + 'l', 4, 0, + '2', 4, 1, 3, 3, + '2', 3, 4, 4, 5, + 'l', 24, 24, + 'l', 4, 43, + '2', 3, 44, 3, 46, + '2', 4, 47, 4, 48, + 'l', 7, 50, + '2', 8, 51, 9, 51, + '2', 11, 51, 11, 50, + 'l', 36, 26, + '2', 37, 25, 37, 24, + '2', 37, 23, 36, 22, + 'e', + /* 0xf067 () */ 56, + 'm', 52, 30, + '2', 54, 30, 55, 29, + '2', 56, 28, 56, 26, + 'l', 56, 22, + '2', 56, 20, 55, 19, + '2', 54, 18, 52, 18, + 'l', 34, 18, + 'l', 34, 0, + '2', 34, -2, 33, -3, + '2', 32, -4, 30, -4, + 'l', 26, -4, + '2', 24, -4, 23, -3, + '2', 22, -2, 22, 0, + 'l', 22, 18, + 'l', 4, 18, + '2', 2, 18, 1, 19, + '2', 0, 20, 0, 22, + 'l', 0, 26, + '2', 0, 28, 1, 29, + '2', 2, 30, 4, 30, + 'l', 22, 30, + 'l', 22, 48, + '2', 22, 50, 23, 51, + '2', 24, 52, 26, 52, + 'l', 30, 52, + '2', 32, 52, 33, 51, + '2', 34, 50, 34, 48, + 'l', 34, 30, + 'l', 52, 30, + 'e', + /* 0xf074 () */ 64, + 'm', 63, 11, + '2', 64, 10, 64, 9, + '2', 64, 8, 63, 7, + 'l', 53, -3, + '2', 52, -4, 50, -4, + '2', 48, -3, 48, -1, + 'l', 48, 4, + 'l', 41, 4, + '2', 40, 4, 40, 5, + 'l', 31, 14, + 'l', 37, 21, + 'l', 44, 14, + 'l', 48, 14, + 'l', 48, 19, + '2', 48, 21, 50, 22, + '2', 52, 23, 53, 21, + 'l', 63, 11, + 'm', 2, 34, + '2', 0, 34, 0, 36, + 'l', 0, 43, + '2', 0, 44, 2, 44, + 'l', 15, 44, + '2', 16, 44, 17, 44, + 'l', 25, 34, + 'l', 19, 27, + 'l', 12, 34, + 'l', 2, 34, + 'm', 48, 34, + 'l', 44, 34, + 'l', 17, 5, + '2', 16, 4, 15, 4, + 'l', 2, 4, + '2', 0, 4, 0, 6, + 'l', 0, 13, + '2', 0, 14, 2, 14, + 'l', 12, 14, + 'l', 40, 44, + '2', 40, 44, 41, 44, + 'l', 48, 44, + 'l', 48, 49, + '2', 48, 51, 50, 52, + '2', 52, 53, 53, 51, + 'l', 63, 41, + '2', 64, 40, 64, 39, + '2', 64, 38, 63, 37, + 'l', 53, 27, + '2', 52, 26, 50, 26, + '2', 48, 27, 48, 29, + 'l', 48, 34, + 'e', + /* 0xf075 () */ 64, + 'm', 9, 44, + '2', 19, 52, 32, 52, + '2', 45, 52, 55, 44, + '2', 64, 37, 64, 26, + '2', 64, 15, 55, 8, + '2', 45, 0, 32, 0, + '2', 25, 0, 19, 2, + '2', 11, -4, 1, -4, + '2', 1, -4, 1, -4, + '2', 0, -4, 0, -3, + '2', 0, -3, 0, -2, + '2', 1, -2, 2, 0, + '2', 3, 1, 5, 4, + '2', 6, 7, 7, 10, + '2', 0, 17, 0, 26, + '2', 0, 37, 9, 44, + 'e', + /* 0xf095 () */ 64, + 'm', 62, 53, + '2', 64, 52, 64, 50, + '2', 64, 26, 47, 9, + '2', 30, -8, 6, -8, + '2', 4, -8, 3, -6, + 'l', 0, 7, + '2', 0, 10, 2, 11, + 'l', 16, 17, + '2', 18, 18, 19, 16, + 'l', 26, 8, + '2', 33, 12, 39, 18, + '2', 44, 23, 48, 31, + 'l', 40, 37, + '2', 38, 38, 39, 40, + 'l', 45, 54, + '2', 46, 57, 49, 56, + 'l', 62, 53, + 'e', + /* 0xf0ac () */ 62, + 'm', 42, 36, + 'l', 20, 36, + '2', 21, 45, 24, 50, + '2', 27, 55, 31, 55, + '2', 35, 55, 38, 50, + '2', 41, 45, 42, 36, + 'm', 19, 16, + '2', 19, 20, 19, 24, + '2', 19, 28, 19, 32, + 'l', 43, 32, + '2', 43, 28, 43, 24, + '2', 43, 20, 43, 16, + 'l', 19, 16, + 'm', 60, 36, + 'l', 46, 36, + '2', 44, 48, 40, 54, + '2', 47, 52, 52, 47, + '2', 57, 42, 60, 36, + 'm', 22, 54, + '2', 18, 48, 16, 36, + 'l', 2, 36, + '2', 5, 42, 10, 47, + '2', 16, 52, 22, 54, + 'm', 61, 32, + '2', 62, 28, 62, 24, + '2', 62, 20, 61, 16, + 'l', 47, 16, + '2', 47, 20, 47, 24, + '2', 47, 28, 47, 32, + 'l', 61, 32, + 'm', 15, 24, + '2', 15, 20, 15, 16, + 'l', 1, 16, + '2', 0, 20, 0, 24, + '2', 0, 28, 1, 32, + 'l', 15, 32, + '2', 15, 28, 15, 24, + 'm', 20, 12, + 'l', 42, 12, + '2', 41, 4, 38, -2, + '2', 35, -7, 31, -7, + '2', 27, -7, 24, -2, + '2', 21, 4, 20, 12, + 'm', 40, -6, + '2', 44, 1, 46, 12, + 'l', 60, 12, + '2', 57, 6, 52, 1, + '2', 47, -4, 40, -6, + 'm', 2, 12, + 'l', 16, 12, + '2', 18, 1, 22, -6, + '2', 16, -4, 10, 1, + '2', 5, 6, 2, 12, + 'e', + /* 0xf0e0 () */ 64, + 'm', 63, 32, + '2', 63, 33, 64, 32, + '2', 64, 32, 64, 32, + 'l', 64, 6, + '2', 64, 4, 62, 2, + '2', 61, 0, 58, 0, + 'l', 6, 0, + '2', 4, 0, 2, 2, + '2', 0, 4, 0, 6, + 'l', 0, 32, + '2', 0, 32, 0, 32, + '2', 1, 32, 1, 32, + '2', 5, 29, 21, 18, + '2', 21, 18, 22, 17, + '2', 24, 16, 25, 15, + '2', 25, 14, 27, 14, + '2', 28, 13, 29, 12, + '2', 31, 12, 32, 12, + '2', 33, 12, 34, 12, + '2', 35, 13, 36, 13, + '2', 37, 14, 39, 14, + '2', 40, 15, 40, 16, + '2', 41, 16, 42, 17, + '2', 43, 18, 44, 18, + '2', 58, 29, 63, 32, + 'm', 32, 16, + '2', 31, 16, 30, 17, + '2', 28, 17, 27, 18, + '2', 26, 19, 25, 20, + '2', 23, 21, 23, 21, + '2', 8, 32, 1, 37, + '2', 0, 38, 0, 40, + 'l', 0, 42, + '2', 0, 45, 2, 46, + '2', 4, 48, 6, 48, + 'l', 58, 48, + '2', 61, 48, 62, 46, + '2', 64, 45, 64, 42, + 'l', 64, 40, + '2', 64, 38, 63, 37, + '2', 57, 33, 41, 21, + '2', 41, 21, 39, 20, + '2', 38, 19, 37, 18, + '2', 36, 17, 34, 17, + '2', 33, 16, 32, 16, + 'e', + /* 0xf0f3 () */ 56, + 'm', 34, -6, + '2', 31, -8, 28, -8, + '2', 25, -8, 22, -6, + '2', 20, -3, 20, 0, + 'l', 36, 0, + '2', 36, -3, 34, -6, + 'm', 55, 11, + '2', 56, 10, 56, 8, + '2', 56, 6, 55, 5, + '2', 54, 4, 52, 4, + 'l', 4, 4, + '2', 3, 4, 2, 5, + '2', 1, 5, 1, 6, + '2', 0, 7, 0, 8, + '2', 0, 10, 1, 11, + '2', 1, 11, 2, 11, + '2', 3, 13, 4, 15, + '2', 6, 16, 7, 20, + '2', 8, 25, 8, 30, + '2', 8, 37, 13, 43, + '2', 17, 48, 24, 49, + 'l', 24, 52, + '2', 24, 54, 25, 55, + '2', 26, 56, 28, 56, + '2', 30, 56, 31, 55, + '2', 32, 54, 32, 52, + 'l', 32, 49, + '2', 37, 49, 40, 46, + '2', 44, 43, 46, 39, + '2', 48, 35, 48, 30, + '2', 48, 25, 49, 20, + '2', 51, 16, 52, 15, + '2', 53, 13, 55, 11, + '2', 55, 11, 55, 11, + 'e', + /* 0xf11c () */ 72, + 'm', 66, 0, + 'l', 6, 0, + '2', 4, 0, 2, 2, + '2', 0, 4, 0, 6, + 'l', 0, 42, + '2', 0, 45, 2, 46, + '2', 4, 48, 6, 48, + 'l', 66, 48, + '2', 69, 48, 70, 46, + '2', 72, 45, 72, 42, + 'l', 72, 6, + '2', 72, 4, 70, 2, + '2', 69, 0, 66, 0, + 'm', 16, 34, + 'l', 16, 39, + '2', 16, 40, 15, 40, + 'l', 10, 40, + '2', 8, 40, 8, 39, + 'l', 8, 34, + '2', 8, 32, 10, 32, + 'l', 15, 32, + '2', 16, 32, 16, 34, + 'm', 28, 34, + 'l', 28, 39, + '2', 28, 40, 27, 40, + 'l', 22, 40, + '2', 20, 40, 20, 39, + 'l', 20, 34, + '2', 20, 32, 22, 32, + 'l', 27, 32, + '2', 28, 32, 28, 34, + 'm', 40, 34, + 'l', 40, 39, + '2', 40, 40, 39, 40, + 'l', 34, 40, + '2', 32, 40, 32, 39, + 'l', 32, 34, + '2', 32, 32, 34, 32, + 'l', 39, 32, + '2', 40, 32, 40, 34, + 'm', 52, 34, + 'l', 52, 39, + '2', 52, 40, 51, 40, + 'l', 46, 40, + '2', 44, 40, 44, 39, + 'l', 44, 34, + '2', 44, 32, 46, 32, + 'l', 51, 32, + '2', 52, 32, 52, 34, + 'm', 64, 34, + 'l', 64, 39, + '2', 64, 40, 63, 40, + 'l', 58, 40, + '2', 56, 40, 56, 39, + 'l', 56, 34, + '2', 56, 32, 58, 32, + 'l', 63, 32, + '2', 64, 32, 64, 34, + 'm', 22, 22, + 'l', 22, 27, + '2', 22, 28, 21, 28, + 'l', 16, 28, + '2', 14, 28, 14, 27, + 'l', 14, 22, + '2', 14, 20, 16, 20, + 'l', 21, 20, + '2', 22, 20, 22, 22, + 'm', 34, 22, + 'l', 34, 27, + '2', 34, 28, 33, 28, + 'l', 28, 28, + '2', 26, 28, 26, 27, + 'l', 26, 22, + '2', 26, 20, 28, 20, + 'l', 33, 20, + '2', 34, 20, 34, 22, + 'm', 46, 22, + 'l', 46, 27, + '2', 46, 28, 45, 28, + 'l', 40, 28, + '2', 38, 28, 38, 27, + 'l', 38, 22, + '2', 38, 20, 40, 20, + 'l', 45, 20, + '2', 46, 20, 46, 22, + 'm', 58, 22, + 'l', 58, 27, + '2', 58, 28, 57, 28, + 'l', 52, 28, + '2', 50, 28, 50, 27, + 'l', 50, 22, + '2', 50, 20, 52, 20, + 'l', 57, 20, + '2', 58, 20, 58, 22, + 'm', 16, 10, + 'l', 16, 15, + '2', 16, 16, 15, 16, + 'l', 10, 16, + '2', 8, 16, 8, 15, + 'l', 8, 10, + '2', 8, 8, 10, 8, + 'l', 15, 8, + '2', 16, 8, 16, 10, + 'm', 52, 10, + 'l', 52, 15, + '2', 52, 16, 51, 16, + 'l', 22, 16, + '2', 20, 16, 20, 15, + 'l', 20, 10, + '2', 20, 8, 22, 8, + 'l', 51, 8, + '2', 52, 8, 52, 10, + 'm', 64, 10, + 'l', 64, 15, + '2', 64, 16, 63, 16, + 'l', 58, 16, + '2', 56, 16, 56, 15, + 'l', 56, 10, + '2', 56, 8, 58, 8, + 'l', 63, 8, + '2', 64, 8, 64, 10, + 'e', + /* 0xf130 () */ 44, + 'm', 31, 16, + '2', 27, 12, 22, 12, + '2', 17, 12, 14, 16, + '2', 10, 19, 10, 24, + 'l', 10, 44, + '2', 10, 49, 14, 53, + '2', 17, 56, 22, 56, + '2', 27, 56, 31, 53, + '2', 34, 49, 34, 44, + 'l', 34, 24, + '2', 34, 19, 31, 16, + 'm', 42, 32, + '2', 43, 32, 43, 31, + '2', 44, 31, 44, 30, + 'l', 44, 24, + '2', 44, 16, 39, 10, + '2', 33, 3, 25, 2, + 'l', 25, -2, + 'l', 32, -2, + '2', 33, -2, 33, -2, + '2', 34, -3, 34, -4, + 'l', 34, -6, + '2', 34, -7, 33, -7, + '2', 33, -8, 32, -8, + 'l', 12, -8, + '2', 11, -8, 11, -7, + '2', 10, -7, 10, -6, + 'l', 10, -4, + '2', 10, -3, 11, -2, + '2', 11, -2, 12, -2, + 'l', 19, -2, + 'l', 19, 2, + '2', 11, 3, 5, 10, + '2', 0, 17, 0, 25, + 'l', 0, 30, + '2', 0, 31, 1, 31, + '2', 1, 32, 2, 32, + 'l', 4, 32, + '2', 5, 32, 5, 31, + '2', 6, 31, 6, 30, + 'l', 6, 25, + '2', 6, 18, 10, 14, + '2', 14, 9, 20, 8, + '2', 28, 7, 33, 12, + '2', 38, 17, 38, 24, + 'l', 38, 30, + '2', 38, 31, 39, 31, + '2', 39, 32, 40, 32, + 'l', 42, 32, + 'e', + /* 0xf185 () */ 64, + 'm', 24, 33, + '2', 27, 36, 32, 36, + '2', 37, 36, 41, 33, + '2', 44, 29, 44, 24, + '2', 44, 19, 41, 16, + '2', 37, 12, 32, 12, + '2', 27, 12, 24, 16, + '2', 20, 19, 20, 24, + '2', 20, 29, 24, 33, + 'm', 63, 26, + '2', 64, 25, 64, 24, + '2', 64, 23, 63, 22, + 'l', 51, 16, + 'l', 55, 4, + '2', 56, 2, 55, 1, + '2', 54, 0, 52, 1, + 'l', 40, 5, + 'l', 34, -7, + '2', 33, -8, 32, -8, + '2', 31, -8, 30, -7, + 'l', 24, 5, + 'l', 12, 1, + '2', 10, 0, 9, 1, + '2', 8, 2, 9, 4, + 'l', 13, 16, + 'l', 1, 22, + '2', 0, 23, 0, 24, + '2', 0, 25, 1, 26, + 'l', 13, 32, + 'l', 9, 44, + '2', 8, 46, 9, 47, + '2', 10, 48, 12, 47, + 'l', 24, 43, + 'l', 30, 55, + '2', 31, 56, 32, 56, + '2', 33, 56, 34, 55, + 'l', 40, 43, + 'l', 52, 47, + '2', 54, 48, 55, 47, + '2', 56, 46, 55, 44, + 'l', 51, 32, + 'l', 63, 26, + 'm', 32, 8, + '2', 39, 8, 43, 13, + '2', 48, 17, 48, 24, + '2', 48, 31, 43, 35, + '2', 39, 40, 32, 40, + '2', 25, 40, 21, 35, + '2', 16, 31, 16, 24, + '2', 16, 17, 21, 13, + '2', 25, 8, 32, 8, + 'e', + /* 0xf1d8 () */ 64, + 'm', 60, 56, + '2', 61, 57, 63, 55, + '2', 64, 54, 64, 53, + 'l', 55, -1, + '2', 55, -3, 53, -4, + '2', 52, -4, 51, -4, + 'l', 35, 3, + 'l', 27, -7, + '2', 26, -8, 24, -8, + '2', 22, -7, 22, -5, + 'l', 22, 5, + 'l', 52, 42, + '2', 53, 42, 52, 43, + '2', 52, 43, 51, 43, + 'l', 15, 11, + 'l', 2, 17, + '2', 0, 18, 0, 19, + '2', 0, 21, 2, 22, + 'l', 60, 56, + 'e', + /* 0xf1da () */ 64, + 'm', 54, 46, + '2', 63, 37, 63, 24, + '2', 63, 11, 54, 2, + '2', 45, -7, 32, -7, + '2', 21, -7, 13, 0, + '2', 12, 1, 11, 2, + '2', 11, 3, 12, 4, + 'l', 14, 6, + '2', 15, 7, 16, 7, + '2', 17, 7, 18, 6, + '2', 24, 1, 32, 1, + '2', 42, 1, 48, 8, + '2', 55, 15, 55, 24, + '2', 55, 34, 48, 40, + '2', 42, 47, 32, 47, + '2', 23, 47, 16, 41, + 'l', 23, 34, + '2', 24, 34, 23, 32, + '2', 23, 31, 21, 31, + 'l', 3, 31, + '2', 2, 31, 2, 32, + '2', 1, 32, 1, 33, + 'l', 1, 51, + '2', 1, 53, 2, 53, + '2', 4, 54, 4, 53, + 'l', 11, 46, + '2', 20, 55, 32, 55, + '2', 45, 55, 54, 46, + 'm', 40, 14, + '2', 40, 13, 38, 13, + '2', 37, 13, 36, 14, + 'l', 28, 20, + 'l', 28, 37, + '2', 28, 38, 29, 39, + '2', 30, 40, 31, 40, + 'l', 33, 40, + '2', 34, 40, 35, 39, + '2', 36, 38, 36, 37, + 'l', 36, 24, + 'l', 41, 20, + '2', 42, 19, 42, 18, + '2', 42, 17, 42, 16, + 'l', 40, 14, + 'e', + /* 0xf1eb () */ 80, + 'm', 79, 37, + '2', 80, 36, 80, 35, + '2', 80, 34, 79, 34, + 'l', 75, 30, + '2', 74, 28, 72, 30, + '2', 59, 42, 40, 42, + '2', 21, 42, 8, 30, + '2', 6, 28, 5, 30, + 'l', 1, 34, + '2', 0, 34, 0, 35, + '2', 0, 36, 1, 37, + '2', 12, 47, 26, 50, + '2', 40, 54, 54, 50, + '2', 69, 47, 79, 37, + 'm', 34, 10, + '2', 37, 12, 40, 12, + '2', 43, 12, 46, 10, + '2', 48, 7, 48, 4, + '2', 48, 1, 46, -2, + '2', 43, -4, 40, -4, + '2', 37, -4, 34, -2, + '2', 32, 1, 32, 4, + '2', 32, 7, 34, 10, + 'm', 65, 23, + '2', 66, 22, 66, 21, + '2', 66, 20, 65, 20, + 'l', 61, 15, + '2', 60, 14, 58, 15, + '2', 51, 22, 40, 22, + '2', 30, 22, 22, 15, + '2', 20, 14, 19, 15, + 'l', 15, 20, + '2', 14, 20, 14, 21, + '2', 14, 22, 15, 23, + '2', 26, 32, 40, 32, + '2', 55, 32, 65, 23, + 'e', + /* 0xf1ec () */ 56, + 'm', 50, 56, + '2', 52, 56, 54, 54, + '2', 56, 52, 56, 50, + 'l', 56, -2, + '2', 56, -4, 54, -6, + '2', 52, -8, 50, -8, + 'l', 6, -8, + '2', 4, -8, 2, -6, + '2', 0, -4, 0, -2, + 'l', 0, 50, + '2', 0, 52, 2, 54, + '2', 4, 56, 6, 56, + 'l', 50, 56, + 'm', 16, 2, + 'l', 16, 6, + '2', 16, 7, 16, 8, + '2', 15, 8, 14, 8, + 'l', 10, 8, + '2', 9, 8, 9, 8, + '2', 8, 7, 8, 6, + 'l', 8, 2, + '2', 8, 1, 9, 1, + '2', 9, 0, 10, 0, + 'l', 14, 0, + '2', 15, 0, 16, 1, + '2', 16, 1, 16, 2, + 'm', 16, 18, + 'l', 16, 22, + '2', 16, 23, 16, 24, + '2', 15, 24, 14, 24, + 'l', 10, 24, + '2', 9, 24, 9, 24, + '2', 8, 23, 8, 22, + 'l', 8, 18, + '2', 8, 17, 9, 17, + '2', 9, 16, 10, 16, + 'l', 14, 16, + '2', 15, 16, 16, 17, + '2', 16, 17, 16, 18, + 'm', 32, 2, + 'l', 32, 6, + '2', 32, 7, 32, 8, + '2', 31, 8, 30, 8, + 'l', 26, 8, + '2', 25, 8, 25, 8, + '2', 24, 7, 24, 6, + 'l', 24, 2, + '2', 24, 1, 25, 1, + '2', 25, 0, 26, 0, + 'l', 30, 0, + '2', 31, 0, 32, 1, + '2', 32, 1, 32, 2, + 'm', 32, 18, + 'l', 32, 22, + '2', 32, 23, 32, 24, + '2', 31, 24, 30, 24, + 'l', 26, 24, + '2', 25, 24, 25, 24, + '2', 24, 23, 24, 22, + 'l', 24, 18, + '2', 24, 17, 25, 17, + '2', 25, 16, 26, 16, + 'l', 30, 16, + '2', 31, 16, 32, 17, + '2', 32, 17, 32, 18, + 'm', 48, 2, + 'l', 48, 22, + '2', 48, 23, 48, 24, + '2', 47, 24, 46, 24, + 'l', 42, 24, + '2', 41, 24, 41, 24, + '2', 40, 23, 40, 22, + 'l', 40, 2, + '2', 40, 1, 41, 1, + '2', 41, 0, 42, 0, + 'l', 46, 0, + '2', 47, 0, 48, 1, + '2', 48, 1, 48, 2, + 'm', 48, 34, + 'l', 48, 46, + '2', 48, 47, 48, 48, + '2', 47, 48, 46, 48, + 'l', 10, 48, + '2', 9, 48, 9, 48, + '2', 8, 47, 8, 46, + 'l', 8, 34, + '2', 8, 33, 9, 33, + '2', 9, 32, 10, 32, + 'l', 46, 32, + '2', 47, 32, 48, 33, + '2', 48, 33, 48, 34, + 'e', + /* 0xf240 () */ 80, + 'm', 68, 36, + 'l', 8, 36, + 'l', 8, 12, + 'l', 68, 12, + 'l', 68, 20, + 'l', 72, 20, + 'l', 72, 28, + 'l', 68, 28, + 'l', 68, 36, + 'm', 70, 44, + '2', 73, 44, 74, 42, + '2', 76, 41, 76, 38, + 'l', 76, 36, + 'l', 77, 36, + '2', 78, 36, 79, 35, + '2', 80, 34, 80, 33, + 'l', 80, 15, + '2', 80, 14, 79, 13, + '2', 78, 12, 77, 12, + 'l', 76, 12, + 'l', 76, 10, + '2', 76, 8, 74, 6, + '2', 73, 4, 70, 4, + 'l', 6, 4, + '2', 4, 4, 2, 6, + '2', 0, 8, 0, 10, + 'l', 0, 38, + '2', 0, 41, 2, 42, + '2', 4, 44, 6, 44, + 'l', 70, 44, + 'm', 64, 32, + 'l', 64, 16, + 'l', 12, 16, + 'l', 12, 32, + 'l', 64, 32, + 'e', + /* 0xf2b9 () */ 56, + 'm', 55, 36, + 'l', 52, 36, + 'l', 52, 28, + 'l', 55, 28, + '2', 56, 28, 56, 27, + 'l', 56, 22, + '2', 56, 20, 55, 20, + 'l', 52, 20, + 'l', 52, 12, + 'l', 55, 12, + '2', 56, 12, 56, 11, + 'l', 56, 6, + '2', 56, 4, 55, 4, + 'l', 52, 4, + 'l', 52, -2, + '2', 52, -4, 50, -6, + '2', 49, -8, 46, -8, + 'l', 6, -8, + '2', 4, -8, 2, -6, + '2', 0, -4, 0, -2, + 'l', 0, 50, + '2', 0, 53, 2, 54, + '2', 4, 56, 6, 56, + 'l', 46, 56, + '2', 49, 56, 50, 54, + '2', 52, 53, 52, 50, + 'l', 52, 44, + 'l', 55, 44, + '2', 56, 44, 56, 43, + 'l', 56, 38, + '2', 56, 36, 55, 36, + 'm', 32, 38, + '2', 29, 40, 26, 40, + '2', 23, 40, 20, 38, + '2', 18, 35, 18, 32, + '2', 18, 29, 20, 26, + '2', 23, 24, 26, 24, + '2', 29, 24, 32, 26, + '2', 34, 29, 34, 32, + '2', 34, 35, 32, 38, + 'm', 40, 10, + 'l', 40, 13, + '2', 40, 16, 38, 18, + '2', 35, 20, 32, 20, + 'l', 31, 20, + '2', 29, 19, 26, 19, + '2', 23, 19, 21, 20, + 'l', 20, 20, + '2', 18, 20, 16, 19, + '2', 14, 18, 13, 16, + '2', 12, 15, 12, 13, + 'l', 12, 10, + '2', 12, 9, 13, 9, + '2', 14, 8, 15, 8, + 'l', 37, 8, + '2', 38, 8, 39, 9, + '2', 40, 9, 40, 10, + 'e', + /* 0xf3cd () */ 40, + 'm', 34, 56, + '2', 37, 56, 38, 54, + '2', 40, 53, 40, 50, + 'l', 40, -2, + '2', 40, -4, 38, -6, + '2', 37, -8, 34, -8, + 'l', 6, -8, + '2', 4, -8, 2, -6, + '2', 0, -4, 0, -2, + 'l', 0, 50, + '2', 0, 53, 2, 54, + '2', 4, 56, 6, 56, + 'l', 34, 56, + 'm', 17, -3, + '2', 18, -4, 20, -4, + '2', 22, -4, 23, -3, + '2', 24, -2, 24, 0, + '2', 24, 2, 23, 3, + '2', 22, 4, 20, 4, + '2', 18, 4, 17, 3, + '2', 16, 2, 16, 0, + '2', 16, -2, 17, -3, + 'm', 34, 10, + 'l', 34, 49, + '2', 34, 50, 33, 50, + 'l', 8, 50, + '2', 6, 50, 6, 49, + 'l', 6, 10, + '2', 6, 8, 8, 8, + 'l', 33, 8, + '2', 34, 8, 34, 10, + 'e', + /* 0xf3dd () */ 80, + 'm', 34, 8, + '2', 35, 9, 38, 11, + 'l', 48, 3, + '2', 33, -8, 14, -8, + '2', 12, -8, 11, -6, + 'l', 8, 7, + '2', 8, 10, 10, 11, + 'l', 24, 17, + '2', 26, 18, 27, 16, + 'l', 34, 8, + 'm', 79, -1, + '2', 80, -2, 80, -2, + '2', 80, -3, 80, -4, + 'l', 77, -7, + '2', 76, -9, 74, -7, + 'l', 1, 49, + '2', -1, 51, 0, 52, + 'l', 3, 55, + '2', 4, 57, 6, 56, + 'l', 50, 21, + '2', 54, 26, 56, 31, + 'l', 48, 37, + '2', 46, 38, 47, 40, + 'l', 53, 54, + '2', 54, 57, 57, 56, + 'l', 70, 53, + '2', 72, 52, 72, 50, + '2', 72, 30, 59, 14, + 'l', 79, -1, + 'e', + /* 0xf55a () */ 80, + 'm', 72, 48, + '2', 75, 48, 78, 46, + '2', 80, 43, 80, 40, + 'l', 80, 8, + '2', 80, 5, 78, 2, + '2', 75, 0, 72, 0, + 'l', 26, 0, + '2', 22, 0, 20, 2, + 'l', 1, 21, + '2', 0, 22, 0, 24, + '2', 0, 26, 1, 27, + 'l', 20, 46, + '2', 22, 48, 26, 48, + 'l', 72, 48, + 'm', 61, 16, + 'l', 54, 24, + 'l', 61, 32, + '2', 63, 33, 61, 35, + 'l', 59, 37, + '2', 57, 39, 56, 37, + 'l', 48, 30, + 'l', 40, 37, + '2', 39, 39, 37, 37, + 'l', 35, 35, + '2', 33, 33, 35, 32, + 'l', 42, 24, + 'l', 35, 16, + '2', 33, 15, 35, 13, + 'l', 37, 11, + '2', 39, 9, 40, 11, + 'l', 48, 18, + 'l', 56, 11, + '2', 57, 9, 59, 11, + 'l', 61, 13, + '2', 63, 15, 61, 16, + 'e', +}; +/* clang-format on */ + +static const twin_charmap_t charmap[] = { + {0x01e0, + { + 0x0000, 0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x006d, 0x0000, + 0x0000, 0x0000, 0x0000, 0x00de, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0127, 0x023c, 0x0000, 0x030d, 0x0000, 0x03e9, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0444, 0x0000, 0x0000, 0x0512, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0625, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0762, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0816, 0x0000, 0x0000, 0x0000, 0x0962, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x09e8, 0x0000, 0x0000, 0x0a2d, 0x0a59, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0ac9, 0x0000, 0x0b0e, 0x0b57, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0ba0, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0c19, + 0x0cd6, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e1, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0d26, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0d6f, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0e50, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0f23, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e2, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0fc3, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x11a0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e3, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1275, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1350, 0x0000, + 0x139f, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x145e, + 0x1506, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e4, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x1689, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e5, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x170c, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01e7, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x17fb, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x1882, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x01ea, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x18f9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, +}; + +twin_font_t twin_font_icons = { + .type = TWIN_FONT_TYPE_TTF, + .name = "Font Awesome 5 Free", + .style = "Solid", + .n_charmap = 8, + .charmap = charmap, + .outlines = outlines, + .ascender = 56, + .descender = -8, + .height = 70, +}; diff --git a/src/font-montserrat.c b/src/font-montserrat.c new file mode 100644 index 00000000..57a595fe --- /dev/null +++ b/src/font-montserrat.c @@ -0,0 +1,2060 @@ +/* + * Generated by tools/ttf/twin-ttf from Montserrat Medium. + * Do not edit: regenerate from the font instead. + */ + +#include "twin_private.h" + +/* clang-format off */ +static const signed char outlines[] = { + 0, + /* 0x20 ( ) */ 17, + 'e', + /* 0x21 (!) */ 17, + 'm', 5, 45, + 'l', 12, 45, + 'l', 11, 14, + 'l', 6, 14, + 'l', 5, 45, + 'm', 9, 0, + '2', 7, 0, 6, 1, + '2', 4, 2, 4, 4, + '2', 4, 5, 6, 7, + '2', 7, 8, 9, 8, + '2', 10, 8, 12, 7, + '2', 13, 5, 13, 4, + '2', 13, 2, 12, 1, + '2', 10, 0, 9, 0, + 'e', + /* 0x22 (") */ 25, + 'm', 4, 45, + 'l', 9, 45, + 'l', 9, 28, + 'l', 4, 28, + 'l', 4, 45, + 'm', 16, 45, + 'l', 21, 45, + 'l', 21, 28, + 'l', 16, 28, + 'l', 16, 45, + 'e', + /* 0x23 (#) */ 45, + 'm', 34, 29, + 'l', 32, 16, + 'l', 41, 16, + 'l', 41, 12, + 'l', 32, 12, + 'l', 30, 0, + 'l', 26, 0, + 'l', 27, 12, + 'l', 15, 12, + 'l', 14, 0, + 'l', 9, 0, + 'l', 11, 12, + 'l', 2, 12, + 'l', 2, 16, + 'l', 11, 16, + 'l', 13, 29, + 'l', 4, 29, + 'l', 4, 33, + 'l', 13, 33, + 'l', 15, 45, + 'l', 19, 45, + 'l', 18, 33, + 'l', 30, 33, + 'l', 31, 45, + 'l', 36, 45, + 'l', 34, 33, + 'l', 43, 33, + 'l', 43, 29, + 'l', 34, 29, + 'm', 29, 29, + 'l', 17, 29, + 'l', 16, 16, + 'l', 28, 16, + 'l', 29, 29, + 'e', + /* 0x24 ($) */ 40, + 'm', 37, 12, + '2', 37, 7, 33, 4, + '2', 30, 0, 22, 0, + 'l', 22, -8, + 'l', 18, -8, + 'l', 18, 0, + '2', 14, 0, 9, 1, + '2', 5, 3, 3, 5, + 'l', 5, 10, + '2', 7, 8, 11, 7, + '2', 14, 5, 18, 5, + 'l', 18, 20, + '2', 14, 21, 11, 22, + '2', 8, 23, 6, 26, + '2', 4, 28, 4, 33, + '2', 4, 38, 7, 41, + '2', 11, 45, 18, 45, + 'l', 18, 52, + 'l', 22, 52, + 'l', 22, 45, + '2', 26, 45, 29, 44, + '2', 33, 43, 35, 41, + 'l', 33, 36, + '2', 28, 39, 22, 40, + 'l', 22, 25, + '2', 27, 24, 30, 22, + '2', 33, 21, 35, 19, + '2', 37, 16, 37, 12, + 'm', 10, 33, + '2', 10, 30, 12, 28, + '2', 14, 27, 18, 26, + 'l', 18, 40, + '2', 14, 39, 12, 37, + '2', 10, 35, 10, 33, + 'm', 22, 5, + '2', 27, 5, 29, 7, + '2', 31, 9, 31, 12, + '2', 31, 15, 29, 16, + '2', 26, 18, 22, 19, + 'l', 22, 5, + 'e', + /* 0x25 (%) */ 54, + 'm', 13, 21, + '2', 8, 21, 5, 24, + '2', 2, 28, 2, 33, + '2', 2, 38, 5, 42, + '2', 8, 45, 13, 45, + '2', 17, 45, 20, 42, + '2', 23, 38, 23, 33, + '2', 23, 27, 20, 24, + '2', 17, 21, 13, 21, + 'm', 40, 45, + 'l', 45, 45, + 'l', 14, 0, + 'l', 9, 0, + 'l', 40, 45, + 'm', 13, 24, + '2', 16, 24, 17, 27, + '2', 19, 29, 19, 33, + '2', 19, 37, 17, 39, + '2', 16, 42, 13, 42, + '2', 10, 42, 8, 39, + '2', 7, 37, 7, 33, + '2', 7, 29, 8, 27, + '2', 10, 24, 13, 24, + 'm', 41, 0, + '2', 36, 0, 34, 3, + '2', 31, 6, 31, 12, + '2', 31, 17, 34, 21, + '2', 36, 24, 41, 24, + '2', 46, 24, 49, 21, + '2', 52, 17, 52, 12, + '2', 52, 6, 49, 3, + '2', 46, 0, 41, 0, + 'm', 41, 3, + '2', 44, 3, 46, 6, + '2', 47, 8, 47, 12, + '2', 47, 16, 46, 18, + '2', 44, 20, 41, 20, + '2', 38, 20, 37, 18, + '2', 35, 16, 35, 12, + '2', 35, 8, 37, 6, + '2', 38, 3, 41, 3, + 'e', + /* 0x26 (&) */ 44, + 'm', 39, -1, + 'l', 34, 5, + '2', 28, -1, 19, -1, + '2', 14, -1, 10, 1, + '2', 7, 2, 5, 5, + '2', 3, 8, 3, 11, + '2', 3, 15, 5, 18, + '2', 8, 22, 14, 25, + '2', 11, 28, 10, 30, + '2', 8, 33, 8, 35, + '2', 8, 40, 12, 42, + '2', 15, 45, 20, 45, + '2', 26, 45, 29, 43, + '2', 32, 40, 32, 36, + '2', 32, 32, 29, 30, + '2', 27, 27, 22, 24, + 'l', 33, 13, + '2', 35, 17, 36, 21, + 'l', 41, 20, + '2', 40, 14, 37, 9, + 'l', 43, 3, + 'l', 39, -1, + 'm', 20, 41, + '2', 17, 41, 16, 39, + '2', 14, 38, 14, 35, + '2', 14, 34, 15, 32, + '2', 16, 30, 19, 28, + '2', 23, 30, 25, 32, + '2', 26, 34, 26, 36, + '2', 26, 38, 25, 39, + '2', 23, 41, 20, 41, + 'm', 19, 5, + '2', 26, 5, 30, 9, + 'l', 17, 22, + '2', 13, 19, 11, 17, + '2', 9, 15, 9, 12, + '2', 9, 9, 12, 7, + '2', 14, 5, 19, 5, + 'e', + /* 0x27 (') */ 13, + 'm', 4, 45, + 'l', 9, 45, + 'l', 9, 28, + 'l', 4, 28, + 'l', 4, 45, + 'e', + /* 0x28 (() */ 22, + 'm', 14, -12, + '2', 10, -7, 8, 1, + '2', 6, 9, 6, 18, + '2', 6, 26, 8, 34, + '2', 10, 42, 14, 47, + 'l', 20, 47, + '2', 16, 41, 14, 33, + '2', 12, 26, 12, 18, + '2', 12, 9, 14, 2, + '2', 16, -6, 20, -12, + 'l', 14, -12, + 'e', + /* 0x29 ()) */ 22, + 'm', 2, -12, + '2', 6, -6, 8, 2, + '2', 10, 9, 10, 18, + '2', 10, 26, 8, 33, + '2', 6, 41, 2, 47, + 'l', 8, 47, + '2', 12, 42, 14, 34, + '2', 16, 26, 16, 18, + '2', 16, 9, 14, 1, + '2', 12, -7, 8, -12, + 'l', 2, -12, + 'e', + /* 0x2a (*) */ 26, + 'm', 17, 36, + 'l', 24, 31, + 'l', 22, 28, + 'l', 15, 32, + 'l', 15, 24, + 'l', 11, 24, + 'l', 11, 32, + 'l', 3, 28, + 'l', 1, 31, + 'l', 9, 36, + 'l', 1, 40, + 'l', 3, 43, + 'l', 11, 39, + 'l', 11, 47, + 'l', 15, 47, + 'l', 15, 39, + 'l', 22, 43, + 'l', 24, 40, + 'l', 17, 36, + 'e', + /* 0x2b (+) */ 37, + 'm', 33, 20, + 'l', 21, 20, + 'l', 21, 8, + 'l', 16, 8, + 'l', 16, 20, + 'l', 4, 20, + 'l', 4, 25, + 'l', 16, 25, + 'l', 16, 36, + 'l', 21, 36, + 'l', 21, 25, + 'l', 33, 25, + 'l', 33, 20, + 'e', + /* 0x2c (,) */ 15, + 'm', 7, 8, + '2', 9, 8, 10, 7, + '2', 12, 6, 12, 4, + '2', 12, 3, 11, 2, + '2', 11, 1, 10, -1, + 'l', 7, -9, + 'l', 3, -9, + 'l', 6, 0, + '2', 5, 1, 4, 2, + '2', 3, 3, 3, 4, + '2', 3, 6, 4, 7, + '2', 6, 8, 7, 8, + 'e', + /* 0x2d (-) */ 25, + 'm', 4, 20, + 'l', 21, 20, + 'l', 21, 15, + 'l', 4, 15, + 'l', 4, 20, + 'e', + /* 0x2e (.) */ 15, + 'm', 7, 0, + '2', 6, 0, 4, 1, + '2', 3, 2, 3, 4, + '2', 3, 6, 4, 7, + '2', 6, 8, 7, 8, + '2', 9, 8, 10, 7, + '2', 12, 6, 12, 4, + '2', 12, 2, 10, 1, + '2', 9, 0, 7, 0, + 'e', + /* 0x2f (/) */ 23, + 'm', 19, 54, + 'l', 25, 54, + 'l', 4, -6, + 'l', -2, -6, + 'l', 19, 54, + 'e', + /* 0x30 (0) */ 43, + 'm', 21, -1, + '2', 16, -1, 12, 2, + '2', 8, 5, 5, 10, + '2', 3, 15, 3, 22, + '2', 3, 30, 5, 35, + '2', 8, 40, 12, 43, + '2', 16, 45, 21, 45, + '2', 27, 45, 31, 43, + '2', 35, 40, 37, 35, + '2', 40, 30, 40, 22, + '2', 40, 15, 37, 10, + '2', 35, 5, 31, 2, + '2', 27, -1, 21, -1, + 'm', 21, 5, + '2', 27, 5, 30, 10, + '2', 33, 14, 33, 22, + '2', 33, 31, 30, 35, + '2', 27, 40, 21, 40, + '2', 16, 40, 13, 35, + '2', 9, 31, 9, 22, + '2', 9, 14, 13, 10, + '2', 16, 5, 21, 5, + 'e', + /* 0x31 (1) */ 24, + 'm', 17, 45, + 'l', 17, 0, + 'l', 11, 0, + 'l', 11, 39, + 'l', 1, 39, + 'l', 1, 45, + 'l', 17, 45, + 'e', + /* 0x32 (2) */ 37, + 'm', 35, 6, + 'l', 35, 0, + 'l', 2, 0, + 'l', 2, 4, + 'l', 21, 22, + '2', 24, 25, 25, 27, + '2', 26, 30, 26, 32, + '2', 26, 36, 24, 38, + '2', 21, 40, 17, 40, + '2', 9, 40, 5, 35, + 'l', 1, 39, + '2', 4, 42, 8, 44, + '2', 12, 45, 17, 45, + '2', 24, 45, 28, 42, + '2', 33, 39, 33, 33, + '2', 33, 29, 31, 26, + '2', 30, 23, 25, 19, + 'l', 12, 6, + 'l', 35, 6, + 'e', + /* 0x33 (3) */ 37, + 'm', 20, 26, + '2', 27, 25, 30, 22, + '2', 34, 18, 34, 13, + '2', 34, 9, 32, 6, + '2', 30, 3, 26, 1, + '2', 22, -1, 17, -1, + '2', 12, -1, 8, 1, + '2', 3, 2, 0, 5, + 'l', 3, 10, + '2', 6, 8, 9, 6, + '2', 13, 5, 17, 5, + '2', 22, 5, 25, 7, + '2', 27, 9, 27, 13, + '2', 27, 17, 25, 19, + '2', 22, 21, 16, 21, + 'l', 13, 21, + 'l', 13, 25, + 'l', 24, 39, + 'l', 2, 39, + 'l', 2, 45, + 'l', 32, 45, + 'l', 32, 40, + 'l', 20, 26, + 'e', + /* 0x34 (4) */ 43, + 'm', 42, 11, + 'l', 34, 11, + 'l', 34, 0, + 'l', 27, 0, + 'l', 27, 11, + 'l', 2, 11, + 'l', 2, 15, + 'l', 25, 45, + 'l', 32, 45, + 'l', 10, 17, + 'l', 28, 17, + 'l', 28, 26, + 'l', 34, 26, + 'l', 34, 17, + 'l', 42, 17, + 'l', 42, 11, + 'e', + /* 0x35 (5) */ 37, + 'm', 16, 27, + '2', 25, 27, 30, 23, + '2', 34, 20, 34, 13, + '2', 34, 9, 33, 6, + '2', 31, 3, 27, 1, + '2', 23, -1, 18, -1, + '2', 13, -1, 8, 1, + '2', 4, 2, 1, 5, + 'l', 4, 10, + '2', 6, 8, 10, 6, + '2', 14, 5, 17, 5, + '2', 23, 5, 25, 7, + '2', 28, 9, 28, 13, + '2', 28, 17, 25, 19, + '2', 22, 21, 15, 21, + 'l', 4, 21, + 'l', 7, 45, + 'l', 32, 45, + 'l', 32, 39, + 'l', 12, 39, + 'l', 11, 27, + 'l', 16, 27, + 'e', + /* 0x36 (6) */ 39, + 'm', 22, 27, + '2', 27, 27, 30, 26, + '2', 34, 24, 36, 21, + '2', 38, 18, 38, 14, + '2', 38, 9, 36, 6, + '2', 34, 3, 30, 1, + '2', 26, -1, 22, -1, + '2', 13, -1, 8, 5, + '2', 3, 11, 3, 22, + '2', 3, 29, 6, 35, + '2', 8, 40, 13, 43, + '2', 18, 45, 24, 45, + '2', 28, 45, 30, 45, + '2', 33, 44, 35, 43, + 'l', 33, 38, + '2', 30, 40, 24, 40, + '2', 17, 40, 13, 35, + '2', 9, 31, 9, 23, + '2', 9, 22, 10, 21, + '2', 11, 24, 15, 26, + '2', 18, 27, 22, 27, + 'm', 22, 5, + '2', 26, 5, 29, 7, + '2', 32, 9, 32, 13, + '2', 32, 17, 29, 20, + '2', 26, 22, 21, 22, + '2', 18, 22, 16, 21, + '2', 14, 20, 12, 18, + '2', 11, 16, 11, 13, + '2', 11, 11, 12, 9, + '2', 14, 7, 16, 6, + '2', 18, 5, 22, 5, + 'e', + /* 0x37 (7) */ 38, + 'm', 36, 45, + 'l', 36, 40, + 'l', 18, 0, + 'l', 11, 0, + 'l', 29, 39, + 'l', 8, 39, + 'l', 8, 31, + 'l', 2, 31, + 'l', 2, 45, + 'l', 36, 45, + 'e', + /* 0x38 (8) */ 41, + 'm', 30, 23, + '2', 34, 22, 36, 19, + '2', 38, 17, 38, 13, + '2', 38, 9, 36, 6, + '2', 34, 3, 30, 1, + '2', 26, -1, 21, -1, + '2', 15, -1, 11, 1, + '2', 7, 3, 5, 6, + '2', 3, 9, 3, 13, + '2', 3, 17, 5, 19, + '2', 7, 22, 11, 23, + '2', 8, 25, 6, 27, + '2', 4, 30, 4, 33, + '2', 4, 37, 6, 40, + '2', 8, 42, 12, 44, + '2', 16, 45, 21, 45, + '2', 25, 45, 29, 44, + '2', 33, 42, 35, 40, + '2', 37, 37, 37, 33, + '2', 37, 30, 35, 27, + '2', 34, 25, 30, 23, + 'm', 21, 40, + '2', 16, 40, 13, 38, + '2', 11, 36, 11, 33, + '2', 11, 30, 13, 28, + '2', 16, 26, 21, 26, + '2', 25, 26, 28, 28, + '2', 31, 30, 31, 33, + '2', 31, 36, 28, 38, + '2', 25, 40, 21, 40, + 'm', 21, 5, + '2', 26, 5, 29, 7, + '2', 32, 9, 32, 13, + '2', 32, 17, 29, 19, + '2', 26, 21, 21, 21, + '2', 15, 21, 12, 19, + '2', 9, 17, 9, 13, + '2', 9, 9, 12, 7, + '2', 15, 5, 21, 5, + 'e', + /* 0x39 (9) */ 39, + 'm', 18, 45, + '2', 27, 45, 31, 39, + '2', 36, 34, 36, 23, + '2', 36, 15, 34, 10, + '2', 31, 5, 26, 2, + '2', 22, -1, 15, -1, + '2', 12, -1, 9, 0, + '2', 6, 1, 4, 2, + 'l', 7, 7, + '2', 10, 5, 15, 5, + '2', 22, 5, 26, 9, + '2', 30, 14, 30, 22, + 'l', 30, 23, + '2', 28, 21, 25, 19, + '2', 21, 18, 17, 18, + '2', 13, 18, 9, 19, + '2', 6, 21, 4, 24, + '2', 2, 27, 2, 31, + '2', 2, 35, 4, 39, + '2', 6, 42, 10, 44, + '2', 13, 45, 18, 45, + 'm', 18, 23, + '2', 21, 23, 24, 24, + '2', 26, 25, 27, 27, + '2', 29, 29, 29, 31, + '2', 29, 34, 27, 36, + '2', 26, 38, 24, 39, + '2', 21, 40, 18, 40, + '2', 13, 40, 11, 38, + '2', 8, 35, 8, 31, + '2', 8, 27, 11, 25, + '2', 14, 23, 18, 23, + 'e', + /* 0x3a (:) */ 15, + 'm', 7, 25, + '2', 6, 25, 4, 27, + '2', 3, 28, 3, 30, + '2', 3, 32, 4, 33, + '2', 6, 34, 7, 34, + '2', 9, 34, 10, 33, + '2', 12, 32, 12, 30, + '2', 12, 28, 10, 27, + '2', 9, 25, 7, 25, + 'm', 7, 0, + '2', 6, 0, 4, 1, + '2', 3, 2, 3, 4, + '2', 3, 6, 4, 7, + '2', 6, 8, 7, 8, + '2', 9, 8, 10, 7, + '2', 12, 6, 12, 4, + '2', 12, 2, 10, 1, + '2', 9, 0, 7, 0, + 'e', + /* 0x3b (;) */ 15, + 'm', 7, 25, + '2', 6, 25, 4, 27, + '2', 3, 28, 3, 30, + '2', 3, 32, 4, 33, + '2', 6, 34, 7, 34, + '2', 9, 34, 10, 33, + '2', 12, 32, 12, 30, + '2', 12, 28, 10, 27, + '2', 9, 25, 7, 25, + 'm', 7, 8, + '2', 9, 8, 10, 7, + '2', 12, 6, 12, 4, + '2', 12, 3, 11, 2, + '2', 11, 1, 10, -1, + 'l', 7, -9, + 'l', 3, -9, + 'l', 6, 0, + '2', 5, 1, 4, 2, + '2', 3, 3, 3, 4, + '2', 3, 6, 4, 7, + '2', 6, 8, 7, 8, + 'e', + /* 0x3c (<) */ 37, + 'm', 33, 31, + 'l', 10, 22, + 'l', 33, 14, + 'l', 33, 8, + 'l', 4, 20, + 'l', 4, 25, + 'l', 33, 36, + 'l', 33, 31, + 'e', + /* 0x3d (=) */ 37, + 'm', 4, 33, + 'l', 33, 33, + 'l', 33, 27, + 'l', 4, 27, + 'l', 4, 33, + 'm', 4, 17, + 'l', 33, 17, + 'l', 33, 12, + 'l', 4, 12, + 'l', 4, 17, + 'e', + /* 0x3e (>) */ 37, + 'm', 33, 25, + 'l', 33, 20, + 'l', 4, 8, + 'l', 4, 14, + 'l', 27, 22, + 'l', 4, 31, + 'l', 4, 36, + 'l', 33, 25, + 'e', + /* 0x3f (?) */ 37, + 'm', 15, 14, + '2', 15, 17, 15, 19, + '2', 16, 21, 18, 22, + '2', 19, 23, 21, 25, + '2', 23, 27, 25, 29, + '2', 26, 31, 26, 33, + '2', 26, 36, 23, 38, + '2', 21, 40, 17, 40, + '2', 9, 40, 5, 35, + 'l', 1, 38, + '2', 3, 42, 7, 43, + '2', 12, 45, 17, 45, + '2', 24, 45, 28, 42, + '2', 32, 39, 32, 34, + '2', 32, 31, 31, 29, + '2', 31, 27, 29, 26, + '2', 28, 25, 26, 23, + '2', 23, 20, 22, 19, + '2', 21, 17, 21, 14, + 'l', 15, 14, + 'm', 18, 0, + '2', 16, 0, 15, 1, + '2', 14, 2, 14, 4, + '2', 14, 5, 15, 7, + '2', 16, 8, 18, 8, + '2', 20, 8, 21, 7, + '2', 22, 5, 22, 4, + '2', 22, 2, 21, 1, + '2', 20, 0, 18, 0, + 'e', + /* 0x40 (@) */ 66, + 'm', 33, 45, + '2', 42, 45, 49, 42, + '2', 55, 38, 59, 32, + '2', 63, 25, 63, 17, + '2', 63, 9, 60, 4, + '2', 57, 0, 52, 0, + '2', 49, 0, 47, 1, + '2', 45, 3, 44, 6, + '2', 42, 3, 39, 1, + '2', 36, 0, 32, 0, + '2', 27, 0, 24, 2, + '2', 20, 4, 18, 8, + '2', 16, 11, 16, 16, + '2', 16, 21, 18, 24, + '2', 20, 28, 24, 30, + '2', 27, 32, 32, 32, + '2', 36, 32, 39, 31, + '2', 42, 30, 44, 27, + 'l', 44, 32, + 'l', 49, 32, + 'l', 49, 9, + '2', 49, 7, 50, 6, + '2', 51, 4, 53, 4, + '2', 56, 4, 57, 8, + '2', 59, 11, 59, 17, + '2', 59, 24, 56, 30, + '2', 52, 35, 47, 38, + '2', 41, 41, 33, 41, + '2', 26, 41, 20, 38, + '2', 14, 35, 11, 29, + '2', 8, 23, 8, 16, + '2', 8, 9, 11, 3, + '2', 14, -2, 20, -5, + '2', 26, -9, 33, -9, + '2', 36, -9, 39, -8, + '2', 43, -7, 45, -6, + 'l', 47, -10, + '2', 44, -11, 40, -12, + '2', 37, -13, 33, -13, + '2', 24, -13, 17, -9, + '2', 11, -5, 7, 1, + '2', 3, 8, 3, 16, + '2', 3, 25, 7, 31, + '2', 11, 38, 18, 42, + '2', 24, 45, 33, 45, + 'm', 33, 5, + '2', 36, 5, 38, 6, + '2', 41, 7, 42, 10, + '2', 44, 13, 44, 16, + '2', 44, 20, 42, 22, + '2', 41, 25, 38, 26, + '2', 36, 27, 33, 27, + '2', 28, 27, 25, 24, + '2', 22, 21, 22, 16, + '2', 22, 11, 25, 8, + '2', 28, 5, 33, 5, + 'e', + /* 0x41 (A) */ 47, + 'm', 35, 11, + 'l', 11, 11, + 'l', 7, 0, + 'l', 0, 0, + 'l', 20, 45, + 'l', 27, 45, + 'l', 47, 0, + 'l', 40, 0, + 'l', 35, 11, + 'm', 33, 16, + 'l', 23, 38, + 'l', 14, 16, + 'l', 33, 16, + 'e', + /* 0x42 (B) */ 48, + 'm', 37, 23, + '2', 41, 22, 43, 19, + '2', 45, 16, 45, 12, + '2', 45, 6, 41, 3, + '2', 36, 0, 28, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 27, 45, + '2', 34, 45, 39, 42, + '2', 43, 39, 43, 33, + '2', 43, 30, 41, 27, + '2', 40, 25, 37, 23, + 'm', 13, 40, + 'l', 13, 25, + 'l', 26, 25, + '2', 31, 25, 34, 27, + '2', 36, 29, 36, 32, + '2', 36, 36, 34, 38, + '2', 31, 40, 26, 40, + 'l', 13, 40, + 'm', 28, 5, + '2', 33, 5, 36, 7, + '2', 39, 9, 39, 13, + '2', 39, 20, 28, 20, + 'l', 13, 20, + 'l', 13, 5, + 'l', 28, 5, + 'e', + /* 0x43 (C) */ 46, + 'm', 27, -1, + '2', 20, -1, 15, 2, + '2', 9, 5, 6, 11, + '2', 3, 16, 3, 22, + '2', 3, 29, 6, 34, + '2', 9, 39, 15, 42, + '2', 20, 45, 27, 45, + '2', 32, 45, 36, 44, + '2', 41, 42, 44, 38, + 'l', 40, 34, + '2', 35, 40, 27, 40, + '2', 22, 40, 18, 37, + '2', 14, 35, 12, 31, + '2', 9, 27, 9, 22, + '2', 9, 18, 12, 14, + '2', 14, 10, 18, 7, + '2', 22, 5, 27, 5, + '2', 35, 5, 40, 11, + 'l', 44, 7, + '2', 41, 3, 36, 1, + '2', 32, -1, 27, -1, + 'e', + /* 0x44 (D) */ 53, + 'm', 7, 45, + 'l', 26, 45, + '2', 33, 45, 38, 42, + '2', 44, 39, 47, 34, + '2', 50, 29, 50, 22, + '2', 50, 16, 47, 11, + '2', 44, 6, 38, 3, + '2', 33, 0, 26, 0, + 'l', 7, 0, + 'l', 7, 45, + 'm', 25, 6, + '2', 31, 6, 35, 8, + '2', 39, 10, 41, 14, + '2', 43, 17, 43, 22, + '2', 43, 27, 41, 31, + '2', 39, 35, 35, 37, + '2', 31, 39, 25, 39, + 'l', 13, 39, + 'l', 13, 6, + 'l', 25, 6, + 'e', + /* 0x45 (E) */ 43, + 'm', 39, 6, + 'l', 39, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 38, 45, + 'l', 38, 39, + 'l', 13, 39, + 'l', 13, 25, + 'l', 36, 25, + 'l', 36, 20, + 'l', 13, 20, + 'l', 13, 6, + 'l', 39, 6, + 'e', + /* 0x46 (F) */ 41, + 'm', 13, 39, + 'l', 13, 24, + 'l', 36, 24, + 'l', 36, 18, + 'l', 13, 18, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 38, 45, + 'l', 38, 39, + 'l', 13, 39, + 'e', + /* 0x47 (G) */ 49, + 'm', 38, 23, + 'l', 44, 23, + 'l', 44, 5, + '2', 41, 2, 36, 1, + '2', 32, -1, 27, -1, + '2', 20, -1, 15, 2, + '2', 9, 5, 6, 11, + '2', 3, 16, 3, 22, + '2', 3, 29, 6, 34, + '2', 9, 39, 15, 42, + '2', 20, 45, 27, 45, + '2', 32, 45, 37, 44, + '2', 41, 42, 44, 38, + 'l', 40, 34, + '2', 35, 40, 27, 40, + '2', 22, 40, 18, 37, + '2', 14, 35, 12, 31, + '2', 9, 27, 9, 22, + '2', 9, 18, 12, 14, + '2', 14, 10, 18, 7, + '2', 22, 5, 27, 5, + '2', 33, 5, 38, 8, + 'l', 38, 23, + 'e', + /* 0x48 (H) */ 52, + 'm', 45, 45, + 'l', 45, 0, + 'l', 39, 0, + 'l', 39, 20, + 'l', 13, 20, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 13, 45, + 'l', 13, 26, + 'l', 39, 26, + 'l', 39, 45, + 'l', 45, 45, + 'e', + /* 0x49 (I) */ 20, + 'm', 7, 45, + 'l', 13, 45, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'e', + /* 0x4a (J) */ 33, + 'm', 12, -1, + '2', 8, -1, 5, 1, + '2', 2, 3, -1, 6, + 'l', 3, 10, + '2', 7, 5, 12, 5, + '2', 16, 5, 18, 7, + '2', 20, 10, 20, 14, + 'l', 20, 39, + 'l', 4, 39, + 'l', 4, 45, + 'l', 26, 45, + 'l', 26, 14, + '2', 26, 7, 23, 3, + '2', 19, -1, 12, -1, + 'e', + /* 0x4b (K) */ 46, + 'm', 21, 20, + 'l', 13, 12, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 13, 45, + 'l', 13, 20, + 'l', 37, 45, + 'l', 45, 45, + 'l', 26, 25, + 'l', 46, 0, + 'l', 38, 0, + 'l', 21, 20, + 'e', + /* 0x4c (L) */ 38, + 'm', 7, 45, + 'l', 13, 45, + 'l', 13, 6, + 'l', 37, 6, + 'l', 37, 0, + 'l', 7, 0, + 'l', 7, 45, + 'e', + /* 0x4d (M) */ 61, + 'm', 48, 0, + 'l', 48, 33, + 'l', 32, 5, + 'l', 29, 5, + 'l', 13, 32, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 12, 45, + 'l', 31, 13, + 'l', 49, 45, + 'l', 54, 45, + 'l', 54, 0, + 'l', 48, 0, + 'e', + /* 0x4e (N) */ 52, + 'm', 45, 45, + 'l', 45, 0, + 'l', 40, 0, + 'l', 13, 33, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 12, 45, + 'l', 39, 11, + 'l', 39, 45, + 'l', 45, 45, + 'e', + /* 0x4f (O) */ 54, + 'm', 27, -1, + '2', 20, -1, 15, 2, + '2', 9, 5, 6, 11, + '2', 3, 16, 3, 22, + '2', 3, 29, 6, 34, + '2', 9, 39, 15, 42, + '2', 20, 45, 27, 45, + '2', 34, 45, 39, 42, + '2', 45, 39, 48, 34, + '2', 51, 29, 51, 22, + '2', 51, 16, 48, 11, + '2', 45, 5, 39, 2, + '2', 34, -1, 27, -1, + 'm', 27, 5, + '2', 32, 5, 36, 7, + '2', 40, 10, 42, 14, + '2', 44, 18, 44, 22, + '2', 44, 27, 42, 31, + '2', 40, 35, 36, 37, + '2', 32, 40, 27, 40, + '2', 22, 40, 18, 37, + '2', 14, 35, 12, 31, + '2', 9, 27, 9, 22, + '2', 9, 18, 12, 14, + '2', 14, 10, 18, 7, + '2', 22, 5, 27, 5, + 'e', + /* 0x50 (P) */ 46, + 'm', 24, 45, + '2', 33, 45, 38, 41, + '2', 43, 36, 43, 29, + '2', 43, 22, 38, 18, + '2', 33, 14, 24, 14, + 'l', 13, 14, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 24, 45, + 'm', 24, 19, + '2', 30, 19, 33, 22, + '2', 36, 24, 36, 29, + '2', 36, 34, 33, 37, + '2', 30, 39, 24, 39, + 'l', 13, 39, + 'l', 13, 19, + 'l', 24, 19, + 'e', + /* 0x51 (Q) */ 54, + 'm', 53, -4, + '2', 51, -7, 48, -8, + '2', 45, -9, 42, -9, + '2', 37, -9, 34, -7, + '2', 30, -5, 25, 0, + '2', 19, 0, 14, 3, + '2', 9, 6, 6, 11, + '2', 3, 16, 3, 22, + '2', 3, 29, 6, 34, + '2', 9, 39, 15, 42, + '2', 20, 45, 27, 45, + '2', 34, 45, 39, 42, + '2', 45, 39, 48, 34, + '2', 51, 29, 51, 22, + '2', 51, 17, 48, 12, + '2', 46, 8, 42, 5, + '2', 38, 1, 33, 0, + '2', 35, -2, 37, -3, + '2', 39, -4, 42, -4, + '2', 46, -4, 50, 0, + 'l', 53, -4, + 'm', 9, 22, + '2', 9, 18, 12, 14, + '2', 14, 10, 18, 7, + '2', 22, 5, 27, 5, + '2', 32, 5, 36, 7, + '2', 40, 10, 42, 14, + '2', 44, 18, 44, 22, + '2', 44, 27, 42, 31, + '2', 40, 35, 36, 37, + '2', 32, 40, 27, 40, + '2', 22, 40, 18, 37, + '2', 14, 35, 12, 31, + '2', 9, 27, 9, 22, + 'e', + /* 0x52 (R) */ 47, + 'm', 37, 0, + 'l', 27, 14, + '2', 25, 14, 24, 14, + 'l', 13, 14, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 45, + 'l', 24, 45, + '2', 33, 45, 38, 41, + '2', 43, 36, 43, 29, + '2', 43, 24, 40, 20, + '2', 38, 17, 33, 15, + 'l', 44, 0, + 'l', 37, 0, + 'm', 24, 19, + '2', 30, 19, 33, 22, + '2', 36, 24, 36, 29, + '2', 36, 34, 33, 37, + '2', 30, 39, 24, 39, + 'l', 13, 39, + 'l', 13, 19, + 'l', 24, 19, + 'e', + /* 0x53 (S) */ 40, + 'm', 20, -1, + '2', 15, -1, 10, 1, + '2', 5, 3, 3, 5, + 'l', 5, 10, + '2', 8, 8, 12, 6, + '2', 16, 5, 20, 5, + '2', 25, 5, 28, 7, + '2', 31, 9, 31, 12, + '2', 31, 14, 29, 16, + '2', 28, 17, 26, 18, + '2', 23, 19, 19, 20, + '2', 14, 21, 11, 22, + '2', 8, 23, 6, 26, + '2', 4, 28, 4, 33, + '2', 4, 36, 6, 39, + '2', 7, 42, 11, 44, + '2', 15, 45, 21, 45, + '2', 25, 45, 29, 44, + '2', 33, 43, 35, 41, + 'l', 33, 36, + '2', 30, 38, 27, 39, + '2', 24, 40, 21, 40, + '2', 15, 40, 13, 38, + '2', 10, 36, 10, 33, + '2', 10, 30, 12, 29, + '2', 13, 28, 15, 27, + '2', 18, 26, 22, 25, + '2', 27, 24, 30, 23, + '2', 33, 21, 35, 19, + '2', 37, 16, 37, 12, + '2', 37, 9, 35, 6, + '2', 33, 3, 29, 1, + '2', 25, -1, 20, -1, + 'e', + /* 0x54 (T) */ 38, + 'm', 16, 39, + 'l', 0, 39, + 'l', 0, 45, + 'l', 37, 45, + 'l', 37, 39, + 'l', 22, 39, + 'l', 22, 0, + 'l', 16, 0, + 'l', 16, 39, + 'e', + /* 0x55 (U) */ 51, + 'm', 25, -1, + '2', 16, -1, 11, 5, + '2', 6, 10, 6, 19, + 'l', 6, 45, + 'l', 13, 45, + 'l', 13, 20, + '2', 13, 5, 25, 5, + '2', 31, 5, 35, 9, + '2', 38, 12, 38, 20, + 'l', 38, 45, + 'l', 44, 45, + 'l', 44, 19, + '2', 44, 10, 39, 5, + '2', 34, -1, 25, -1, + 'e', + /* 0x56 (V) */ 46, + 'm', 46, 45, + 'l', 26, 0, + 'l', 20, 0, + 'l', 0, 45, + 'l', 7, 45, + 'l', 23, 8, + 'l', 39, 45, + 'l', 46, 45, + 'e', + /* 0x57 (W) */ 72, + 'm', 70, 45, + 'l', 55, 0, + 'l', 48, 0, + 'l', 36, 36, + 'l', 24, 0, + 'l', 17, 0, + 'l', 2, 45, + 'l', 9, 45, + 'l', 21, 8, + 'l', 33, 45, + 'l', 39, 45, + 'l', 52, 8, + 'l', 64, 45, + 'l', 70, 45, + 'e', + /* 0x58 (X) */ 43, + 'm', 35, 0, + 'l', 21, 18, + 'l', 8, 0, + 'l', 1, 0, + 'l', 18, 23, + 'l', 2, 45, + 'l', 9, 45, + 'l', 22, 28, + 'l', 34, 45, + 'l', 41, 45, + 'l', 25, 23, + 'l', 42, 0, + 'l', 35, 0, + 'e', + /* 0x59 (Y) */ 41, + 'm', 24, 15, + 'l', 24, 0, + 'l', 18, 0, + 'l', 18, 16, + 'l', 0, 45, + 'l', 7, 45, + 'l', 21, 21, + 'l', 35, 45, + 'l', 42, 45, + 'l', 24, 15, + 'e', + /* 0x5a (Z) */ 42, + 'm', 40, 6, + 'l', 40, 0, + 'l', 3, 0, + 'l', 3, 4, + 'l', 31, 39, + 'l', 3, 39, + 'l', 3, 45, + 'l', 39, 45, + 'l', 39, 40, + 'l', 11, 6, + 'l', 40, 6, + 'e', + /* 0x5b ([) */ 21, + 'm', 7, 47, + 'l', 20, 47, + 'l', 20, 42, + 'l', 13, 42, + 'l', 13, -7, + 'l', 20, -7, + 'l', 20, -12, + 'l', 7, -12, + 'l', 7, 47, + 'e', + /* 0x5c (\) */ 23, + 'm', 19, -6, + 'l', -2, 54, + 'l', 3, 54, + 'l', 24, -6, + 'l', 19, -6, + 'e', + /* 0x5d (]) */ 21, + 'm', 1, -12, + 'l', 1, -7, + 'l', 8, -7, + 'l', 8, 42, + 'l', 1, 42, + 'l', 1, 47, + 'l', 15, 47, + 'l', 15, -12, + 'l', 1, -12, + 'e', + /* 0x5e (^) */ 37, + 'm', 28, 9, + 'l', 19, 31, + 'l', 10, 9, + 'l', 5, 9, + 'l', 16, 36, + 'l', 21, 36, + 'l', 33, 9, + 'l', 28, 9, + 'e', + /* 0x5f (_) */ 32, + 'm', 0, 0, + 'l', 32, 0, + 'l', 32, -4, + 'l', 0, -4, + 'l', 0, 0, + 'e', + /* 0x60 (`) */ 38, + 'm', 7, 47, + 'l', 15, 47, + 'l', 24, 39, + 'l', 18, 39, + 'l', 7, 47, + 'e', + /* 0x61 (a) */ 38, + 'm', 18, 34, + '2', 25, 34, 29, 31, + '2', 33, 27, 33, 20, + 'l', 33, 0, + 'l', 27, 0, + 'l', 27, 4, + '2', 25, 2, 22, 1, + '2', 20, 0, 16, 0, + '2', 10, 0, 7, 2, + '2', 3, 5, 3, 10, + '2', 3, 14, 6, 17, + '2', 10, 20, 17, 20, + 'l', 27, 20, + 'l', 27, 21, + '2', 27, 25, 24, 27, + '2', 22, 29, 17, 29, + '2', 14, 29, 12, 28, + '2', 9, 27, 7, 25, + 'l', 4, 30, + '2', 7, 32, 10, 33, + '2', 14, 34, 18, 34, + 'm', 17, 4, + '2', 20, 4, 23, 6, + '2', 25, 7, 27, 10, + 'l', 27, 15, + 'l', 17, 15, + '2', 9, 15, 9, 10, + '2', 9, 7, 11, 6, + '2', 13, 4, 17, 4, + 'e', + /* 0x62 (b) */ 44, + 'm', 24, 34, + '2', 29, 34, 33, 32, + '2', 37, 30, 39, 26, + '2', 41, 22, 41, 17, + '2', 41, 12, 39, 8, + '2', 37, 4, 33, 2, + '2', 29, 0, 24, 0, + '2', 20, 0, 17, 1, + '2', 14, 3, 12, 5, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 47, + 'l', 12, 47, + 'l', 12, 29, + '2', 14, 31, 17, 33, + '2', 20, 34, 24, 34, + 'm', 23, 5, + '2', 27, 5, 29, 6, + '2', 32, 8, 33, 11, + '2', 35, 13, 35, 17, + '2', 35, 20, 33, 23, + '2', 32, 26, 29, 27, + '2', 27, 29, 23, 29, + '2', 20, 29, 17, 27, + '2', 15, 26, 13, 23, + '2', 12, 20, 12, 17, + '2', 12, 13, 13, 11, + '2', 15, 8, 17, 6, + '2', 20, 5, 23, 5, + 'e', + /* 0x63 (c) */ 37, + 'm', 21, 0, + '2', 15, 0, 11, 2, + '2', 7, 4, 5, 8, + '2', 3, 12, 3, 17, + '2', 3, 22, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 21, 34, + '2', 25, 34, 29, 32, + '2', 32, 31, 34, 27, + 'l', 30, 24, + '2', 28, 26, 26, 28, + '2', 23, 29, 21, 29, + '2', 17, 29, 15, 27, + '2', 12, 26, 10, 23, + '2', 9, 20, 9, 17, + '2', 9, 13, 10, 11, + '2', 12, 8, 15, 6, + '2', 17, 5, 21, 5, + '2', 23, 5, 26, 6, + '2', 28, 7, 30, 10, + 'l', 34, 7, + '2', 32, 3, 29, 1, + '2', 25, 0, 21, 0, + 'e', + /* 0x64 (d) */ 44, + 'm', 38, 47, + 'l', 38, 0, + 'l', 32, 0, + 'l', 32, 5, + '2', 30, 3, 27, 1, + '2', 24, 0, 20, 0, + '2', 15, 0, 11, 2, + '2', 7, 4, 5, 8, + '2', 3, 12, 3, 17, + '2', 3, 22, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 20, 34, + '2', 23, 34, 27, 33, + '2', 30, 31, 32, 29, + 'l', 32, 47, + 'l', 38, 47, + 'm', 20, 5, + '2', 24, 5, 26, 6, + '2', 29, 8, 30, 11, + '2', 32, 13, 32, 17, + '2', 32, 20, 30, 23, + '2', 29, 26, 26, 27, + '2', 24, 29, 20, 29, + '2', 17, 29, 14, 27, + '2', 12, 26, 10, 23, + '2', 9, 20, 9, 17, + '2', 9, 13, 10, 11, + '2', 12, 8, 14, 6, + '2', 17, 5, 20, 5, + 'e', + /* 0x65 (e) */ 39, + 'm', 36, 17, + '2', 36, 16, 36, 15, + 'l', 9, 15, + '2', 9, 10, 13, 8, + '2', 16, 5, 21, 5, + '2', 27, 5, 31, 9, + 'l', 34, 5, + '2', 32, 2, 29, 1, + '2', 25, 0, 21, 0, + '2', 16, 0, 11, 2, + '2', 7, 4, 5, 8, + '2', 3, 12, 3, 17, + '2', 3, 22, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 20, 34, + '2', 25, 34, 28, 32, + '2', 32, 30, 34, 26, + '2', 36, 22, 36, 17, + 'm', 20, 29, + '2', 15, 29, 12, 26, + '2', 9, 24, 9, 19, + 'l', 31, 19, + '2', 30, 24, 27, 26, + '2', 24, 29, 20, 29, + 'e', + /* 0x66 (f) */ 23, + 'm', 18, 43, + '2', 13, 43, 13, 37, + 'l', 13, 34, + 'l', 23, 34, + 'l', 23, 29, + 'l', 13, 29, + 'l', 13, 0, + 'l', 7, 0, + 'l', 7, 29, + 'l', 1, 29, + 'l', 1, 34, + 'l', 7, 34, + 'l', 7, 37, + '2', 7, 42, 10, 45, + '2', 13, 48, 18, 48, + '2', 20, 48, 22, 47, + '2', 24, 47, 25, 46, + 'l', 23, 41, + '2', 21, 43, 18, 43, + 'e', + /* 0x67 (g) */ 44, + 'm', 38, 34, + 'l', 38, 5, + '2', 38, -4, 34, -9, + '2', 29, -13, 21, -13, + '2', 16, -13, 12, -11, + '2', 7, -10, 4, -8, + 'l', 7, -3, + '2', 10, -5, 13, -6, + '2', 17, -7, 20, -7, + '2', 26, -7, 29, -5, + '2', 32, -2, 32, 4, + 'l', 32, 7, + '2', 30, 4, 27, 3, + '2', 24, 1, 20, 1, + '2', 15, 1, 11, 3, + '2', 7, 6, 5, 9, + '2', 3, 13, 3, 18, + '2', 3, 23, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 20, 34, + '2', 24, 34, 27, 33, + '2', 30, 31, 32, 29, + 'l', 32, 34, + 'l', 38, 34, + 'm', 21, 7, + '2', 24, 7, 27, 8, + '2', 29, 10, 31, 12, + '2', 32, 15, 32, 18, + '2', 32, 23, 29, 26, + '2', 26, 29, 21, 29, + '2', 15, 29, 12, 26, + '2', 9, 23, 9, 18, + '2', 9, 15, 10, 12, + '2', 12, 10, 15, 8, + '2', 17, 7, 21, 7, + 'e', + /* 0x68 (h) */ 44, + 'm', 24, 34, + '2', 30, 34, 34, 30, + '2', 38, 27, 38, 20, + 'l', 38, 0, + 'l', 32, 0, + 'l', 32, 19, + '2', 32, 24, 30, 26, + '2', 27, 29, 23, 29, + '2', 18, 29, 15, 26, + '2', 12, 23, 12, 17, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 47, + 'l', 12, 47, + 'l', 12, 29, + '2', 14, 32, 17, 33, + '2', 20, 34, 24, 34, + 'e', + /* 0x69 (i) */ 18, + 'm', 6, 34, + 'l', 12, 34, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 34, + 'm', 9, 40, + '2', 7, 40, 6, 42, + '2', 5, 43, 5, 44, + '2', 5, 46, 6, 47, + '2', 7, 48, 9, 48, + '2', 11, 48, 12, 47, + '2', 13, 46, 13, 45, + '2', 13, 43, 12, 42, + '2', 11, 40, 9, 40, + 'e', + /* 0x6a (j) */ 18, + 'm', 1, -13, + '2', -1, -13, -3, -12, + '2', -5, -12, -6, -11, + 'l', -4, -6, + '2', -2, -8, 1, -8, + '2', 4, -8, 5, -6, + '2', 6, -5, 6, -2, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, -2, + '2', 12, -7, 9, -10, + '2', 7, -13, 1, -13, + 'm', 9, 40, + '2', 7, 40, 6, 42, + '2', 5, 43, 5, 44, + '2', 5, 46, 6, 47, + '2', 7, 48, 9, 48, + '2', 11, 48, 12, 47, + '2', 13, 46, 13, 45, + '2', 13, 43, 12, 42, + '2', 11, 40, 9, 40, + 'e', + /* 0x6b (k) */ 39, + 'm', 19, 16, + 'l', 12, 9, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 47, + 'l', 12, 47, + 'l', 12, 17, + 'l', 30, 34, + 'l', 38, 34, + 'l', 24, 20, + 'l', 39, 0, + 'l', 32, 0, + 'l', 19, 16, + 'e', + /* 0x6c (l) */ 18, + 'm', 6, 47, + 'l', 12, 47, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 47, + 'e', + /* 0x6d (m) */ 68, + 'm', 48, 34, + '2', 55, 34, 58, 31, + '2', 62, 27, 62, 20, + 'l', 62, 0, + 'l', 56, 0, + 'l', 56, 19, + '2', 56, 24, 54, 26, + '2', 51, 29, 47, 29, + '2', 43, 29, 40, 26, + '2', 37, 23, 37, 17, + 'l', 37, 0, + 'l', 31, 0, + 'l', 31, 19, + '2', 31, 24, 29, 26, + '2', 26, 29, 22, 29, + '2', 17, 29, 15, 26, + '2', 12, 23, 12, 17, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, 29, + '2', 14, 31, 17, 33, + '2', 20, 34, 23, 34, + '2', 27, 34, 30, 33, + '2', 34, 31, 35, 28, + '2', 37, 31, 41, 33, + '2', 44, 34, 48, 34, + 'e', + /* 0x6e (n) */ 44, + 'm', 24, 34, + '2', 30, 34, 34, 30, + '2', 38, 27, 38, 20, + 'l', 38, 0, + 'l', 32, 0, + 'l', 32, 19, + '2', 32, 24, 30, 26, + '2', 27, 29, 23, 29, + '2', 18, 29, 15, 26, + '2', 12, 23, 12, 17, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, 29, + '2', 14, 31, 17, 33, + '2', 20, 34, 24, 34, + 'e', + /* 0x6f (o) */ 41, + 'm', 20, 0, + '2', 15, 0, 11, 2, + '2', 7, 4, 5, 8, + '2', 3, 12, 3, 17, + '2', 3, 22, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 20, 34, + '2', 25, 34, 29, 32, + '2', 33, 30, 36, 26, + '2', 38, 22, 38, 17, + '2', 38, 12, 36, 8, + '2', 33, 4, 29, 2, + '2', 25, 0, 20, 0, + 'm', 20, 5, + '2', 24, 5, 26, 6, + '2', 29, 8, 30, 11, + '2', 32, 13, 32, 17, + '2', 32, 20, 30, 23, + '2', 29, 26, 26, 27, + '2', 24, 29, 20, 29, + '2', 17, 29, 14, 27, + '2', 12, 26, 10, 23, + '2', 9, 20, 9, 17, + '2', 9, 13, 10, 11, + '2', 12, 8, 14, 6, + '2', 17, 5, 20, 5, + 'e', + /* 0x70 (p) */ 44, + 'm', 24, 34, + '2', 29, 34, 33, 32, + '2', 37, 30, 39, 26, + '2', 41, 22, 41, 17, + '2', 41, 12, 39, 8, + '2', 37, 4, 33, 2, + '2', 29, 0, 24, 0, + '2', 20, 0, 17, 1, + '2', 14, 2, 12, 5, + 'l', 12, -12, + 'l', 6, -12, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, 29, + '2', 14, 31, 17, 33, + '2', 20, 34, 24, 34, + 'm', 23, 5, + '2', 27, 5, 29, 6, + '2', 32, 8, 33, 11, + '2', 35, 13, 35, 17, + '2', 35, 20, 33, 23, + '2', 32, 26, 29, 27, + '2', 27, 29, 23, 29, + '2', 20, 29, 17, 27, + '2', 15, 26, 13, 23, + '2', 12, 20, 12, 17, + '2', 12, 13, 13, 11, + '2', 15, 8, 17, 6, + '2', 20, 5, 23, 5, + 'e', + /* 0x71 (q) */ 44, + 'm', 38, 34, + 'l', 38, -12, + 'l', 32, -12, + 'l', 32, 5, + '2', 30, 2, 26, 1, + '2', 23, 0, 20, 0, + '2', 15, 0, 11, 2, + '2', 7, 4, 5, 8, + '2', 3, 12, 3, 17, + '2', 3, 22, 5, 26, + '2', 7, 30, 11, 32, + '2', 15, 34, 20, 34, + '2', 24, 34, 27, 33, + '2', 30, 31, 32, 29, + 'l', 32, 34, + 'l', 38, 34, + 'm', 20, 5, + '2', 24, 5, 26, 6, + '2', 29, 8, 30, 11, + '2', 32, 13, 32, 17, + '2', 32, 20, 30, 23, + '2', 29, 26, 26, 27, + '2', 24, 29, 20, 29, + '2', 17, 29, 14, 27, + '2', 12, 26, 10, 23, + '2', 9, 20, 9, 17, + '2', 9, 13, 10, 11, + '2', 12, 8, 14, 6, + '2', 17, 5, 20, 5, + 'e', + /* 0x72 (r) */ 26, + 'm', 12, 28, + '2', 13, 31, 16, 33, + '2', 20, 34, 24, 34, + 'l', 24, 28, + '2', 24, 28, 23, 28, + '2', 18, 28, 15, 25, + '2', 12, 22, 12, 17, + 'l', 12, 0, + 'l', 6, 0, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, 28, + 'e', + /* 0x73 (s) */ 32, + 'm', 16, 0, + '2', 11, 0, 8, 1, + '2', 4, 2, 2, 4, + 'l', 4, 8, + '2', 6, 7, 10, 6, + '2', 13, 5, 16, 5, + '2', 24, 5, 24, 10, + '2', 24, 11, 23, 12, + '2', 22, 13, 20, 13, + '2', 19, 14, 16, 14, + '2', 12, 15, 9, 16, + '2', 6, 17, 4, 18, + '2', 3, 20, 3, 24, + '2', 3, 29, 6, 31, + '2', 10, 34, 17, 34, + '2', 20, 34, 24, 33, + '2', 27, 33, 29, 31, + 'l', 26, 26, + '2', 22, 29, 17, 29, + '2', 13, 29, 11, 28, + '2', 9, 26, 9, 24, + '2', 9, 23, 10, 22, + '2', 11, 21, 13, 20, + '2', 14, 20, 17, 19, + '2', 22, 19, 24, 18, + '2', 27, 17, 28, 15, + '2', 30, 13, 30, 10, + '2', 30, 5, 26, 2, + '2', 22, 0, 16, 0, + 'e', + /* 0x74 (t) */ 26, + 'm', 25, 2, + '2', 24, 1, 22, 0, + '2', 20, 0, 17, 0, + '2', 12, 0, 10, 2, + '2', 7, 5, 7, 10, + 'l', 7, 29, + 'l', 1, 29, + 'l', 1, 34, + 'l', 7, 34, + 'l', 7, 41, + 'l', 13, 41, + 'l', 13, 34, + 'l', 23, 34, + 'l', 23, 29, + 'l', 13, 29, + 'l', 13, 10, + '2', 13, 8, 14, 6, + '2', 16, 5, 18, 5, + '2', 21, 5, 23, 6, + 'l', 25, 2, + 'e', + /* 0x75 (u) */ 43, + 'm', 37, 34, + 'l', 37, 0, + 'l', 32, 0, + 'l', 32, 5, + '2', 30, 2, 27, 1, + '2', 24, 0, 20, 0, + '2', 13, 0, 9, 3, + '2', 6, 7, 6, 14, + 'l', 6, 34, + 'l', 12, 34, + 'l', 12, 15, + '2', 12, 10, 14, 8, + '2', 16, 5, 21, 5, + '2', 26, 5, 28, 8, + '2', 31, 11, 31, 16, + 'l', 31, 34, + 'l', 37, 34, + 'e', + /* 0x76 (v) */ 36, + 'm', 36, 34, + 'l', 21, 0, + 'l', 15, 0, + 'l', 0, 34, + 'l', 6, 34, + 'l', 18, 7, + 'l', 30, 34, + 'l', 36, 34, + 'e', + /* 0x77 (w) */ 58, + 'm', 57, 34, + 'l', 44, 0, + 'l', 39, 0, + 'l', 29, 26, + 'l', 19, 0, + 'l', 13, 0, + 'l', 0, 34, + 'l', 6, 34, + 'l', 16, 7, + 'l', 26, 34, + 'l', 31, 34, + 'l', 42, 7, + 'l', 52, 34, + 'l', 57, 34, + 'e', + /* 0x78 (x) */ 35, + 'm', 27, 0, + 'l', 18, 13, + 'l', 8, 0, + 'l', 1, 0, + 'l', 14, 17, + 'l', 2, 34, + 'l', 8, 34, + 'l', 18, 22, + 'l', 27, 34, + 'l', 34, 34, + 'l', 21, 17, + 'l', 34, 0, + 'l', 27, 0, + 'e', + /* 0x79 (y) */ 36, + 'm', 36, 34, + 'l', 19, -3, + '2', 17, -9, 14, -11, + '2', 11, -13, 7, -13, + '2', 5, -13, 2, -12, + '2', 0, -11, -1, -10, + 'l', 1, -5, + '2', 4, -8, 7, -8, + '2', 10, -8, 11, -6, + '2', 13, -5, 14, -2, + 'l', 15, 0, + 'l', 0, 34, + 'l', 6, 34, + 'l', 18, 7, + 'l', 30, 34, + 'l', 36, 34, + 'e', + /* 0x7a (z) */ 33, + 'm', 31, 5, + 'l', 31, 0, + 'l', 3, 0, + 'l', 3, 4, + 'l', 23, 29, + 'l', 3, 29, + 'l', 3, 34, + 'l', 30, 34, + 'l', 30, 30, + 'l', 10, 5, + 'l', 31, 5, + 'e', + /* 0x7b ({) */ 22, + 'm', 15, 23, + '2', 15, 21, 14, 19, + '2', 13, 18, 11, 18, + '2', 13, 17, 14, 16, + '2', 15, 14, 15, 12, + 'l', 15, -2, + '2', 15, -7, 20, -7, + 'l', 21, -7, + 'l', 21, -12, + 'l', 18, -12, + '2', 14, -12, 11, -10, + '2', 9, -8, 9, -3, + 'l', 9, 12, + '2', 9, 15, 6, 15, + 'l', 4, 15, + 'l', 4, 20, + 'l', 6, 20, + '2', 9, 20, 9, 23, + 'l', 9, 38, + '2', 9, 43, 11, 45, + '2', 14, 47, 18, 47, + 'l', 21, 47, + 'l', 21, 42, + 'l', 20, 42, + '2', 15, 42, 15, 37, + 'l', 15, 23, + 'e', + /* 0x7c (|) */ 19, + 'm', 7, 47, + 'l', 12, 47, + 'l', 12, -12, + 'l', 7, -12, + 'l', 7, 47, + 'e', + /* 0x7d (}) */ 22, + 'm', 19, 20, + 'l', 19, 15, + 'l', 17, 15, + '2', 14, 15, 14, 12, + 'l', 14, -3, + '2', 14, -8, 11, -10, + '2', 9, -12, 4, -12, + 'l', 1, -12, + 'l', 1, -7, + 'l', 3, -7, + '2', 8, -7, 8, -2, + 'l', 8, 12, + '2', 8, 14, 8, 16, + '2', 9, 17, 11, 18, + '2', 9, 18, 8, 19, + '2', 8, 21, 8, 23, + 'l', 8, 37, + '2', 8, 42, 3, 42, + 'l', 1, 42, + 'l', 1, 47, + 'l', 4, 47, + '2', 9, 47, 11, 45, + '2', 14, 43, 14, 38, + 'l', 14, 23, + '2', 14, 20, 17, 20, + 'l', 19, 20, + 'e', + /* 0x7e (~) */ 37, + 'm', 25, 17, + '2', 23, 17, 21, 18, + '2', 19, 19, 17, 21, + '2', 16, 22, 15, 23, + '2', 14, 23, 12, 23, + '2', 10, 23, 9, 22, + '2', 8, 20, 8, 17, + 'l', 4, 17, + '2', 4, 23, 6, 25, + '2', 8, 28, 12, 28, + '2', 14, 28, 16, 27, + '2', 18, 26, 20, 24, + '2', 22, 23, 23, 22, + '2', 24, 22, 25, 22, + '2', 27, 22, 28, 23, + '2', 29, 25, 30, 28, + 'l', 34, 28, + '2', 33, 22, 31, 20, + '2', 29, 17, 25, 17, + 'e', + /* 0xb0 (°) */ 27, + 'm', 13, 24, + '2', 10, 24, 8, 26, + '2', 6, 27, 4, 30, + '2', 3, 32, 3, 35, + '2', 3, 38, 4, 40, + '2', 6, 42, 8, 44, + '2', 10, 45, 13, 45, + '2', 16, 45, 19, 44, + '2', 21, 42, 23, 40, + '2', 24, 38, 24, 35, + '2', 24, 32, 23, 30, + '2', 21, 27, 19, 26, + '2', 16, 24, 13, 24, + 'm', 13, 28, + '2', 16, 28, 18, 30, + '2', 20, 32, 20, 35, + '2', 20, 38, 18, 40, + '2', 16, 42, 13, 42, + '2', 10, 42, 8, 40, + '2', 6, 38, 6, 35, + '2', 6, 32, 8, 30, + '2', 10, 28, 13, 28, + 'e', + /* 0xb7 (·) */ 17, + 'm', 9, 13, + '2', 7, 13, 5, 14, + '2', 4, 16, 4, 18, + '2', 4, 20, 5, 21, + '2', 7, 22, 9, 22, + '2', 10, 22, 12, 21, + '2', 13, 20, 13, 18, + '2', 13, 16, 12, 14, + '2', 10, 13, 9, 13, + 'e', + /* 0x2013 (–) */ 32, + 'm', 0, 20, + 'l', 32, 20, + 'l', 32, 15, + 'l', 0, 15, + 'l', 0, 20, + 'e', + /* 0x2014 (—) */ 64, + 'm', 0, 20, + 'l', 64, 20, + 'l', 64, 15, + 'l', 0, 15, + 'l', 0, 20, + 'e', + /* 0x2022 (•) */ 20, + 'm', 10, 12, + '2', 8, 12, 6, 13, + '2', 4, 15, 4, 18, + '2', 4, 20, 6, 22, + '2', 8, 23, 10, 23, + '2', 12, 23, 14, 22, + '2', 16, 20, 16, 18, + '2', 16, 15, 14, 13, + '2', 12, 12, 10, 12, + 'e', + /* 0x2026 (…) */ 44, + 'm', 7, 0, + '2', 6, 0, 4, 1, + '2', 3, 2, 3, 4, + '2', 3, 6, 4, 7, + '2', 6, 8, 7, 8, + '2', 9, 8, 10, 7, + '2', 12, 6, 12, 4, + '2', 12, 2, 10, 1, + '2', 9, 0, 7, 0, + 'm', 22, 0, + '2', 20, 0, 19, 1, + '2', 18, 2, 18, 4, + '2', 18, 6, 19, 7, + '2', 20, 8, 22, 8, + '2', 24, 8, 25, 7, + '2', 26, 6, 26, 4, + '2', 26, 2, 25, 1, + '2', 24, 0, 22, 0, + 'm', 37, 0, + '2', 35, 0, 34, 1, + '2', 33, 2, 33, 4, + '2', 33, 6, 34, 7, + '2', 35, 8, 37, 8, + '2', 39, 8, 40, 7, + '2', 41, 6, 41, 4, + '2', 41, 2, 40, 1, + '2', 39, 0, 37, 0, + 'e', +}; +/* clang-format on */ + +static const twin_charmap_t charmap[] = { + {0x0000, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0003, 0x003f, 0x005f, + 0x00c7, 0x0173, 0x0230, 0x02de, 0x02ef, 0x0322, 0x0355, 0x0390, 0x03b9, + 0x03ef, 0x0400, 0x042d, 0x043e, 0x04aa, 0x04c1, 0x0512, 0x0573, 0x05a5, + 0x0603, 0x069f, 0x06bf, 0x077e, 0x0818, 0x0870, 0x08d1, 0x08eb, 0x090b, + 0x0925, 0x09b0, 0x0abe, 0x0ae7, 0x0b58, 0x0bbd, 0x0c13, 0x0c3c, 0x0c5f, + 0x0cca, 0x0cf3, 0x0d04, 0x0d3e, 0x0d67, 0x0d7e, 0x0daa, 0x0dcd, 0x0e4d, + 0x0e95, 0x0f3b, 0x0f91, 0x1032, 0x104f, 0x1089, 0x10a3, 0x10cf, 0x10f8, + 0x1118, 0x113b, 0x1158, 0x1169, 0x1186, 0x11a0, 0x11b1, 0x11c2, 0x1241, + 0x12c6, 0x1335, 0x13ba, 0x142a, 0x1471, 0x1514, 0x1559, 0x1595, 0x15f4, + 0x161d, 0x162e, 0x16a4, 0x16e9, 0x1769, 0x17ee, 0x1873, 0x18a3, 0x1930, + 0x197c, 0x19c1, 0x19db, 0x1a07, 0x1a30, 0x1a70, 0x1a93, 0x1afb, 0x1b0c, + 0x1b74, 0x0000, + }}, + {0x0001, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x1bcf, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x1c3b, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, + {0x0040, + { + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x1c68, 0x1c79, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x1c8a, 0x0000, + 0x0000, 0x0000, 0x1cb7, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, + }}, +}; + +twin_font_t twin_font_montserrat = { + .type = TWIN_FONT_TYPE_TTF, + .name = "Montserrat", + .style = "Medium", + .n_charmap = 3, + .charmap = charmap, + .outlines = outlines, + .ascender = 62, + .descender = -16, + .height = 78, +}; diff --git a/src/font.c b/src/font.c index 682209d0..8eead9ff 100644 --- a/src/font.c +++ b/src/font.c @@ -25,6 +25,14 @@ typedef struct _twin_text_info { twin_fixed_t snap_y[TWIN_GLYPH_MAX_SNAP_Y]; } twin_text_info_t; +/* 1 / m in 16.16; unscaled text (m = 1) needs no division */ +static twin_fixed_t _twin_reciprocal(twin_fixed_t m) +{ + if (m == TWIN_FIXED_ONE || m == -TWIN_FIXED_ONE) + return m; + return twin_fixed_div(TWIN_FIXED_ONE, m); +} + static void _twin_text_compute_info(twin_path_t *path, const twin_font_t *font, twin_text_info_t *info) @@ -61,8 +69,7 @@ static void _twin_text_compute_info(twin_path_t *path, } info->scale.x = twin_fixed_mul(path->state.font_size, path->state.matrix.m[0][xi]); - info->reverse_scale.x = - twin_fixed_div(TWIN_FIXED_ONE, path->state.matrix.m[0][xi]); + info->reverse_scale.x = _twin_reciprocal(path->state.matrix.m[0][xi]); if (info->scale.x < 0) { info->scale.x = -info->scale.x; info->reverse_scale.x = -info->reverse_scale.x; @@ -71,8 +78,7 @@ static void _twin_text_compute_info(twin_path_t *path, } info->scale.y = twin_fixed_mul(path->state.font_size, path->state.matrix.m[1][yi]); - info->reverse_scale.y = - twin_fixed_div(TWIN_FIXED_ONE, path->state.matrix.m[1][yi]); + info->reverse_scale.y = _twin_reciprocal(path->state.matrix.m[1][yi]); if (info->scale.y < 0) { info->scale.y = -info->scale.y; info->reverse_scale.y = -info->reverse_scale.y; @@ -224,7 +230,12 @@ static bool twin_find_ucs4_page(twin_font_t *font, uint32_t page) bool twin_has_ucs4(twin_font_t *font, twin_ucs4_t ucs4) { - return twin_find_ucs4_page(font, twin_ucs_page(ucs4)); + if (!twin_find_ucs4_page(font, twin_ucs_page(ucs4))) + return false; + /* Offset 0 marks a code point the font does not cover, except in stroke + * fonts, whose U+0000 glyph sits at offset 0; TrueType tables reserve it */ + return font->cur_page->offsets[twin_ucs_char_in_page(ucs4)] != 0 || + (ucs4 == 0 && font->type == TWIN_FONT_TYPE_STROKE); } #define SNAPX(p) _snap(path, p, snap_x, nsnap_x) @@ -240,9 +251,26 @@ static const signed char *_twin_g_base(twin_font_t *font, twin_ucs4_t ucs4) return font->outlines + font->cur_page->offsets[idx]; } +/* + * Glyph encodings + * + * Stroke glyphs start with a header (left, right, ascent, descent, snap + * counts, snap positions) in 2.6 fixed point, Y pointing down. + * TrueType glyphs, as emitted by tools/ttf/twin-ttf, start with a single + * advance byte and use Y pointing up; ascent and descent are per font. + */ +static inline bool _twin_font_is_ttf(const twin_font_t *font) +{ + return font->type == TWIN_FONT_TYPE_TTF; +} + static twin_fixed_t _twin_glyph_width(twin_text_info_t *info, + const twin_font_t *font, const signed char *b) { + if (_twin_font_is_ttf(font)) + return FX(b[0], info); + twin_fixed_t right = FX(twin_glyph_right(b), info) + info->pen.x * 2; twin_fixed_t right_side_bearing; twin_fixed_t width; @@ -256,11 +284,54 @@ static twin_fixed_t _twin_glyph_width(twin_text_info_t *info, return width; } -void twin_text_metrics_ucs4(twin_path_t *path, - twin_ucs4_t ucs4, - twin_text_metrics_t *m) +/* + * Horizontal extent of a TrueType glyph's outline in font units. Control + * points are included, so this bounds the ink (TrueType places on-curve + * points at the extrema, making it exact in practice). Returns false for a + * glyph without ink, such as a space. + */ +static bool _twin_ttf_glyph_x_bounds(const signed char *b, + signed char *min_x, + signed char *max_x) { - twin_font_t *font = g_twin_font; + const signed char *g = b + 1; /* skip the advance byte */ + bool any = false; + for (;;) { + int points; + switch (*g++) { + case 'm': + case 'l': + points = 1; + break; + case '2': + points = 2; + break; + case 'c': + points = 3; + break; + default: + return any; + } + for (; points; points--, g += 2) { + if (!any || g[0] < *min_x) + *min_x = g[0]; + if (!any || g[0] > *max_x) + *max_x = g[0]; + any = true; + } + } +} + +static inline twin_font_t *_twin_path_font(const twin_path_t *path) +{ + return path->state.font ? path->state.font : g_twin_font; +} + +static void twin_text_metrics_ucs4(twin_path_t *path, + twin_ucs4_t ucs4, + twin_text_metrics_t *m) +{ + twin_font_t *font = _twin_path_font(path); const signed char *b = _twin_g_base(font, ucs4); twin_text_info_t info; twin_fixed_t left, right, ascent, descent; @@ -270,6 +341,22 @@ void twin_text_metrics_ucs4(twin_path_t *path, twin_fixed_t margin_x, margin_y; _twin_text_compute_info(path, font, &info); + if (_twin_font_is_ttf(font)) { + /* TrueType: bearings from the outline; vertical metrics from the font + */ + twin_fixed_t adv = FX(b[0], &info); + signed char min_x, max_x; + if (_twin_ttf_glyph_x_bounds(b, &min_x, &max_x)) { + m->left_side_bearing = FX(min_x, &info); + m->right_side_bearing = FX(max_x, &info); + } else { + m->left_side_bearing = m->right_side_bearing = 0; /* no ink */ + } + m->width = adv; + m->ascent = m->font_ascent = FY(font->ascender, &info); + m->descent = m->font_descent = FY(-font->descender, &info); + return; + } if (info.snap) _twin_text_compute_snap(&info, b); @@ -314,12 +401,12 @@ static const signed char *twin_glyph_draw(const twin_font_t *font, { if (font->type == TWIN_FONT_TYPE_STROKE) return twin_glyph_snap_y(b) + twin_glyph_n_snap_y(b); - return b + 4; + return b + 1; /* TrueType: skip the advance byte */ } void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) { - twin_font_t *font = g_twin_font; + twin_font_t *font = _twin_path_font(path); const signed char *b = _twin_g_base(font, ucs4); const signed char *g = twin_glyph_draw(font, b); twin_spoint_t origin; @@ -328,6 +415,8 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) twin_path_t *pen = NULL; twin_fixed_t width; twin_text_info_t info; + /* TrueType outlines are Y-up; flip them into screen space */ + int ys = _twin_font_is_ttf(font) ? -1 : 1; _twin_text_compute_info(path, font, &info); if (info.snap) @@ -347,7 +436,7 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) switch ((op = *g++)) { case 'm': x1 = FX(*g++, &info); - y1 = FY(*g++, &info); + y1 = FY(ys * *g++, &info); if (info.snap) { x1 = _twin_snap(x1, info.snap_x, info.n_snap_x); y1 = _twin_snap(y1, info.snap_y, info.n_snap_y); @@ -356,7 +445,7 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) continue; case 'l': x1 = FX(*g++, &info); - y1 = FY(*g++, &info); + y1 = FY(ys * *g++, &info); if (info.snap) { x1 = _twin_snap(x1, info.snap_x, info.n_snap_x); y1 = _twin_snap(y1, info.snap_y, info.n_snap_y); @@ -365,11 +454,11 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) continue; case 'c': x3 = FX(*g++, &info); - y3 = FY(*g++, &info); + y3 = FY(ys * *g++, &info); x2 = FX(*g++, &info); - y2 = FY(*g++, &info); + y2 = FY(ys * *g++, &info); x1 = FX(*g++, &info); - y1 = FY(*g++, &info); + y1 = FY(ys * *g++, &info); if (info.snap) { x3 = _twin_snap(x3, info.snap_x, info.n_snap_x); y3 = _twin_snap(y3, info.snap_y, info.n_snap_y); @@ -382,11 +471,11 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) continue; case '2': _x1 = FX(*g++, &info); - _y1 = FY(*g++, &info); + _y1 = FY(ys * *g++, &info); x3 = x1 + 2 * (_x1 - x1) / 3; y3 = y1 + 2 * (_y1 - y1) / 3; x1 = FX(*g++, &info); - y1 = FY(*g++, &info); + y1 = FY(ys * *g++, &info); x2 = x1 + 2 * (_x1 - x1) / 3; y2 = y1 + 2 * (_y1 - y1) / 3; twin_path_curve(stroke, x3, y3, x2, y2, x1, y1); @@ -406,7 +495,7 @@ void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4) twin_path_append(path, stroke); twin_path_destroy(stroke); - width = _twin_glyph_width(&info, b); + width = _twin_glyph_width(&info, font, b); _twin_path_smove(path, origin.x + _twin_matrix_dx(&info.matrix, width, 0), origin.y + _twin_matrix_dy(&info.matrix, width, 0)); @@ -420,7 +509,7 @@ twin_fixed_t twin_width_ucs4(twin_path_t *path, twin_ucs4_t ucs4) return metrics.width; } -static int _twin_utf8_to_ucs4(const char *src_orig, twin_ucs4_t *dst) +int _twin_utf8_to_ucs4(const char *src_orig, twin_ucs4_t *dst) { const char *src = src_orig; char s; @@ -486,7 +575,19 @@ twin_fixed_t twin_width_utf8(twin_path_t *path, const char *string) int len; twin_ucs4_t ucs4; twin_fixed_t w = 0; - + twin_font_t *font = _twin_path_font(path); + + /* Measuring adds no points, so the pen and scale stay put: for TrueType + * the advance is just the scaled width byte, set up once per string. */ + if (_twin_font_is_ttf(font)) { + twin_text_info_t info; + _twin_text_compute_info(path, font, &info); + while ((len = _twin_utf8_to_ucs4(string, &ucs4)) > 0) { + w += FX(_twin_g_base(font, ucs4)[0], &info); + string += len; + } + return w; + } while ((len = _twin_utf8_to_ucs4(string, &ucs4)) > 0) { w += twin_width_ucs4(path, ucs4); string += len; diff --git a/src/font_default.c b/src/font_default.c index 4a7d7ec6..1f557cb1 100644 --- a/src/font_default.c +++ b/src/font_default.c @@ -4,7 +4,7 @@ * All rights reserved. */ -#include "twin.h" +#include "twin_private.h" /* clang-format off */ static const signed char outlines[] = { diff --git a/src/icon.c b/src/icon.c index 30afd937..0dc36795 100644 --- a/src/icon.c +++ b/src/icon.c @@ -107,7 +107,7 @@ const uint16_t _twin_icons[] = { TWIN_CLOSE_POS, TWIN_RESIZE_POS, }; -#define V(i) (g[i] << 10) +#define V(i) (g[i] * 1024) /* not a shift: g[i] may be negative */ #define TWIN_ICON_FILL 0xff808080 #define TWIN_ICON_STROKE 0xff202020 diff --git a/src/image-gif.c b/src/image-gif.c index ba2c5b2b..e8b1e7c7 100644 --- a/src/image-gif.c +++ b/src/image-gif.c @@ -58,11 +58,30 @@ typedef struct { entry_t *entries; } table_t; +/* 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) +{ + 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; +} + static uint16_t read_num(int fd) { uint8_t bytes[2]; - read(fd, bytes, 2); + gif_read(fd, bytes, 2); return bytes[0] + (((uint16_t) bytes[1]) << 8); } @@ -83,13 +102,13 @@ static twin_gif_t *gif_open(const char *fname) setmode(fd, O_BINARY); #endif /* Header */ - read(fd, sigver, 3); + gif_read(fd, sigver, 3); if (memcmp(sigver, "GIF", 3) != 0) { log_error("Invalid signature"); goto fail; } /* Version */ - read(fd, sigver, 3); + gif_read(fd, sigver, 3); if (memcmp(sigver, "89a", 3) != 0) { log_error("Invalid version"); goto fail; @@ -97,8 +116,13 @@ static twin_gif_t *gif_open(const char *fname) /* 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 */ - read(fd, &fdsz, 1); + gif_read(fd, &fdsz, 1); /* Presence of GCT */ if (!(fdsz & 0x80)) { log_error("No global color table"); @@ -110,9 +134,9 @@ static twin_gif_t *gif_open(const char *fname) /* GCT Size */ gct_sz = 1 << ((fdsz & 0x07) + 1); /* Background Color Index */ - read(fd, &bgidx, 1); + gif_read(fd, &bgidx, 1); /* Aspect Ratio */ - read(fd, &aspect, 1); + gif_read(fd, &aspect, 1); /* Create twin_gif_t Structure. */ gif = twin_calloc(1, sizeof(*gif)); if (!gif) @@ -123,10 +147,10 @@ static twin_gif_t *gif_open(const char *fname) gif->depth = depth; /* Read GCT */ gif->gct.size = gct_sz; - read(fd, gif->gct.colors, 3 * gif->gct.size); + gif_read(fd, gif->gct.colors, 3 * gif->gct.size); gif->palette = &gif->gct; gif->bgindex = bgidx; - gif->frame = twin_calloc(4, width * height); + gif->frame = twin_calloc(4, (size_t) width * height); if (!gif->frame) { twin_free(gif); goto fail; @@ -152,7 +176,7 @@ static void discard_sub_blocks(twin_gif_t *gif) uint8_t size; do { - read(gif->fd, &size, 1); + gif_read(gif->fd, &size, 1); lseek(gif->fd, size, SEEK_CUR); } while (size); } @@ -220,11 +244,11 @@ static uint16_t get_key(twin_gif_t *gif, if (rpad == 0) { /* Update byte. */ if (*sub_len == 0) { - read(gif->fd, sub_len, 1); /* Must be nonzero! */ + gif_read(gif->fd, sub_len, 1); /* Must be nonzero! */ if (*sub_len == 0) return 0x1000; } - read(gif->fd, byte, 1); + gif_read(gif->fd, byte, 1); (*sub_len)--; } frag_size = MIN(key_size - bits_read, 8 - rpad); @@ -272,7 +296,7 @@ static int read_image_data(twin_gif_t *gif, int interlace) entry_t entry = {0}; off_t start, end; - read(gif->fd, &byte, 1); + gif_read(gif->fd, &byte, 1); key_size = (int) byte; if (key_size < 2 || key_size > 8) return -1; @@ -284,6 +308,8 @@ static int read_image_data(twin_gif_t *gif, int interlace) clear = 1 << key_size; stop = clear + 1; table = table_new(key_size); + if (!table) + return -1; key_size++; init_key_size = key_size; sub_len = shift = 0; @@ -318,6 +344,9 @@ static int read_image_data(twin_gif_t *gif, int interlace) 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) + break; for (i = 0; i < str_len; i++) { p = frm_off + entry.length - 1; x = p % gif->fw; @@ -336,7 +365,7 @@ static int read_image_data(twin_gif_t *gif, int interlace) } twin_free(table); if (key == stop) - read(gif->fd, &sub_len, 1); /* Must be zero! */ + gif_read(gif->fd, &sub_len, 1); /* Must be zero! */ lseek(gif->fd, end, SEEK_SET); return 0; } @@ -362,14 +391,14 @@ static int read_image(twin_gif_t *gif) gif->fw = MIN(gif->fw, gif->width - gif->fx); gif->fh = MIN(gif->fh, gif->height - gif->fy); - read(gif->fd, &fisrz, 1); + gif_read(gif->fd, &fisrz, 1); interlace = fisrz & 0x40; /* Ignore Sort Flag. */ /* Local Color table_t? */ if (fisrz & 0x80) { /* Read LCT */ gif->lct.size = 1 << ((fisrz & 0x07) + 1); - read(gif->fd, gif->lct.colors, 3 * gif->lct.size); + gif_read(gif->fd, gif->lct.colors, 3 * gif->lct.size); gif->palette = &gif->lct; } else gif->palette = &gif->gct; @@ -429,12 +458,12 @@ static void read_graphic_control_ext(twin_gif_t *gif) /* Discard block size (always 0x04). */ lseek(gif->fd, 1, SEEK_CUR); - read(gif->fd, &rdit, 1); + gif_read(gif->fd, &rdit, 1); gif->gce.disposal = (rdit >> 2) & 3; gif->gce.input = rdit & 2; gif->gce.transparency = rdit & 1; gif->gce.delay = read_num(gif->fd); - read(gif->fd, &gif->gce.tindex, 1); + gif_read(gif->fd, &gif->gce.tindex, 1); /* Skip block terminator. */ lseek(gif->fd, 1, SEEK_CUR); } @@ -453,9 +482,9 @@ static void read_application_ext(twin_gif_t *gif) /* Discard block size (always 0x0B). */ lseek(gif->fd, 1, SEEK_CUR); /* Application Identifier. */ - read(gif->fd, app_id, 8); + gif_read(gif->fd, app_id, 8); /* Application Authentication Code. */ - read(gif->fd, app_auth_code, 3); + gif_read(gif->fd, 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); @@ -471,7 +500,7 @@ static void read_ext(twin_gif_t *gif) { uint8_t label; - if (read(gif->fd, &label, 1) < 1) + if (!gif_read(gif->fd, &label, 1)) return; switch (label) { case 0x01: @@ -497,14 +526,15 @@ static int gif_get_frame(twin_gif_t *gif) char sep; dispose(gif); - read(gif->fd, &sep, 1); + if (!gif_read(gif->fd, &sep, 1)) + return -1; /* truncated: no trailer */ while (sep != ',') { if (sep == ';') return 0; if (sep != '!') return -1; read_ext(gif); - if (read(gif->fd, &sep, 1) < 1) + if (!gif_read(gif->fd, &sep, 1)) return -1; } if (read_image(gif) == -1) @@ -514,7 +544,7 @@ static int gif_get_frame(twin_gif_t *gif) static void gif_render_frame(twin_gif_t *gif, uint8_t *buffer) { - memcpy(buffer, gif->canvas, gif->width * gif->height * 3); + memcpy(buffer, gif->canvas, (size_t) gif->width * gif->height * 3); render_frame_rect(gif, buffer); } @@ -554,9 +584,16 @@ static twin_animation_t *_twin_animation_from_gif_file(const char *path) 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)) + 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); @@ -575,7 +612,7 @@ static twin_animation_t *_twin_animation_from_gif_file(const char *path) gif_rewind(gif); uint8_t *color, *frame; - frame = twin_malloc(gif->width * gif->height * 3); + frame = twin_malloc((size_t) gif->width * gif->height * 3); if (!frame) { twin_free(anim->frame_delays); twin_free(anim->frames); @@ -624,7 +661,7 @@ static twin_animation_t *_twin_animation_from_gif_file(const char *path) anim->frame_delays[i] = gif->gce.delay * 10; gif_get_frame(gif); } - anim->iter = twin_animation_iter_init(anim); + anim->iter = _twin_animation_iter_init(anim); if (!anim->iter) { /* Free all allocated pixmaps */ for (twin_count_t i = 0; i < frame_count; i++) @@ -655,10 +692,12 @@ twin_pixmap_t *_twin_gif_to_pixmap(const char *filepath, twin_format_t fmt) } twin_animation_t *gif = _twin_animation_from_gif_file(filepath); if (gif) { - /* Allocate pixmap */ + /* 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); diff --git a/src/image-png.c b/src/image-png.c index fadbdf18..b81c04f5 100644 --- a/src/image-png.c +++ b/src/image-png.c @@ -11,6 +11,46 @@ #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 +} + +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]); + } +} + typedef struct { int fd; } twin_png_priv_t; @@ -45,12 +85,12 @@ static void _convertBGRtoARGB(uint8_t *data, int width, int height) twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) { uint8_t signature[8]; - int rb = 0; png_structp png = NULL; png_infop info = NULL; - twin_pixmap_t *pix = NULL; + /* 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; - png_bytep *rowp = NULL; int fd = open(filepath, O_RDONLY); if (fd < 0) @@ -79,11 +119,15 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) png_set_read_fn(png, &priv, _twin_png_read); png_set_sig_bytes(png, n); + /* A pixmap's sides are twin_coord_t: refuse anything larger up front */ + png_set_user_limits(png, INT16_MAX, INT16_MAX); 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; if (depth == 16) png_set_strip_16(png); @@ -101,7 +145,6 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) case TWIN_A8: if (ctype != PNG_COLOR_TYPE_GRAY || depth != 8) goto bail_free; - rb = width; break; case TWIN_RGB16: /* unsupported for now */ @@ -116,18 +159,19 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) if (depth != 8) goto bail_free; - rb = width * 4; break; } - rowp = twin_malloc(height * sizeof(png_bytep)); + rowp = twin_malloc((size_t) height * sizeof(png_bytep)); if (!rowp) goto bail_free; - pix = twin_pixmap_create(fmt, width, height); + pix = twin_pixmap_create(fmt, (twin_coord_t) width, (twin_coord_t) height); if (!pix) goto bail_free; - for (size_t i = 0; i < height; i++) - rowp[i] = pix->p.b + rb * i; + /* libpng writes width * twin_bytes_per_pixel(fmt) bytes per row; rows + * sit at the pixmap's stride, which is at least that, maybe padded */ + for (twin_coord_t i = 0; i < pix->height; i++) + rowp[i] = pix->p.b + (size_t) pix->stride * i; png_read_image(png, rowp); @@ -140,6 +184,7 @@ twin_pixmap_t *_twin_png_to_pixmap(const char *filepath, twin_format_t fmt) #endif twin_premultiply_alpha(pix); } + goto bail_free; bail_free: twin_free(rowp); diff --git a/src/image-tvg.c b/src/image-tvg.c index b5164af1..8363b796 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -3,7 +3,6 @@ * Copyright (c) 2024 National Cheng Kung University, Taiwan * All rights reserved. */ -#include #include #include #include @@ -13,9 +12,15 @@ #include "twin.h" #include "twin_private.h" -#define D(x) twin_double_to_fixed(x) +/* Pens for a zero line width: 0.01 and 0.1 units in 16.16, as the float + * loader truncated them */ +#define TVG_HAIRLINE ((twin_fixed_t) 655) +#define TVG_THIN_LINE ((twin_fixed_t) 6553) + #define GET_COLOR(ctx, idx) ctx->colors[idx] -#define PIXEL_ARGB(a, r, g, b) (((a) << 24) | ((r) << 16) | ((g) << 8) | (b)) +#define PIXEL_ARGB(a, r, g, b) \ + (((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 \ @@ -193,9 +198,9 @@ enum { /* flag indicating the sweep direction 0=left, 1=right */ #define TVG_ARC_SWEEP(x) ((x >> 1) & 1) -/* F32 pixel format */ +/* F32 pixel format: IEEE 754 single bits, decoded without floating point */ typedef struct { - float r, g, b, a; + uint32_t r, g, b, a; } tvg_f32_pixel_t; /* rgba32 color struct */ @@ -203,25 +208,15 @@ typedef struct { uint8_t r, g, b, a; } tvg_rgba32_t; -/* TVG internal color struct */ -typedef struct { - float r, g, b; -} tvg_rgb_t; - -/* TVG internal color struct with alpha channel */ -typedef struct { - float r, g, b, a; -} tvg_rgba_t; - /* coordinate */ typedef struct { - float x, y; + twin_fixed_t x, y; } tvg_point_t; /* rectangle */ typedef struct { - float x, y; - float width, height; + twin_fixed_t x, y; + twin_fixed_t width, height; } tvg_rect_t; /* gradient data */ @@ -251,7 +246,7 @@ typedef struct { /* line header */ typedef struct { tvg_style_t style; - float line_width; + twin_fixed_t line_width; size_t size; } tvg_line_header_t; @@ -259,7 +254,7 @@ typedef struct { typedef struct { tvg_style_t fill_style; tvg_style_t line_style; - float line_width; + twin_fixed_t line_width; size_t size; } tvg_line_fill_header_t; @@ -356,56 +351,55 @@ static tvg_result_t tvg_read_coord(tvg_context_t *ctx, uint32_t *out_raw_value) } } +/* An IEEE 754 single as 16.16, truncated toward zero like a float to + * integer conversion of f * 65536 */ +static twin_fixed_t tvg_f32_to_fixed(uint32_t bits) +{ + int exp = (int) ((bits >> 23) & 0xff); + if (exp == 0) + return 0; /* zero or subnormal: below 2^-126 */ + uint32_t mant = (bits & 0x7fffff) | 0x800000; + /* value = mant * 2^(exp - 150); times 2^16 */ + int shift = exp - 134; + uint32_t v; + if (shift >= 8) + v = 0x7fffffff; /* out of range: saturate */ + else if (shift >= 0) + v = mant << shift; + else if (shift > -32) + v = mant >> -shift; + else + v = 0; + return (bits >> 31) ? -(twin_fixed_t) v : (twin_fixed_t) v; +} + +/* An F32 color channel as 255 * channel, truncated */ +static uint8_t tvg_f32_channel(uint32_t bits) +{ + return (uint8_t) twin_fixed_to_int( + twin_fixed_mul(twin_int_to_fixed(255), tvg_f32_to_fixed(bits))); +} + static tvg_result_t tvg_read_color(tvg_context_t *ctx, twin_argb32_t *out_color) { size_t read; - twin_fixed_t max_color_f = twin_int_to_fixed(255); switch (ctx->color_encoding) { case TVG_COLOR_F32: { tvg_f32_pixel_t data; READ_VALUE(read, &data, sizeof(tvg_f32_pixel_t)); - /* 255.0 * A */ - twin_fixed_t a_f = - twin_fixed_mul(max_color_f, twin_double_to_fixed(data.a)); - /* 255.0 * R */ - twin_fixed_t r_f = - twin_fixed_mul(max_color_f, twin_double_to_fixed(data.r)); - /* 255.0 * G */ - twin_fixed_t g_f = - twin_fixed_mul(max_color_f, twin_double_to_fixed(data.g)); - /* 255.0 * B */ - twin_fixed_t b_f = - twin_fixed_mul(max_color_f, twin_double_to_fixed(data.b)); - uint8_t a = (uint8_t) twin_fixed_to_int(a_f); - uint8_t r = (uint8_t) twin_fixed_to_int(r_f); - uint8_t g = (uint8_t) twin_fixed_to_int(g_f); - uint8_t b = (uint8_t) twin_fixed_to_int(b_f); - *out_color = PIXEL_ARGB(a, r, g, b); + *out_color = + PIXEL_ARGB(tvg_f32_channel(data.a), tvg_f32_channel(data.r), + tvg_f32_channel(data.g), tvg_f32_channel(data.b)); return TVG_SUCCESS; } case TVG_COLOR_U565: { uint16_t data; READ_VALUE(read, &data, 2); - /* (255.0 * R) / 15.0 */ - twin_fixed_t r_f = twin_fixed_div( - twin_fixed_mul(max_color_f, - twin_double_to_fixed((float) TVG_RGB16_R(data))), - twin_int_to_fixed(15)); - /* (255.0 * G) / 31.0 */ - twin_fixed_t g_f = twin_fixed_div( - twin_fixed_mul(max_color_f, - twin_double_to_fixed((float) TVG_RGB16_G(data))), - twin_int_to_fixed(31)); - /* (255.0 * B) / 15.0 */ - twin_fixed_t b_f = twin_fixed_div( - twin_fixed_mul(max_color_f, - twin_double_to_fixed((float) TVG_RGB16_B(data))), - twin_int_to_fixed(15)); - uint8_t a = 0xff; - uint8_t r = (uint8_t) twin_fixed_to_int(r_f); - uint8_t g = (uint8_t) twin_fixed_to_int(g_f); - uint8_t b = (uint8_t) twin_fixed_to_int(b_f); - *out_color = PIXEL_ARGB(a, r, g, b); + /* 255 * R / 15, 255 * G / 31, 255 * B / 15, truncated */ + uint8_t r = (uint8_t) (255 * TVG_RGB16_R(data) / 15); + uint8_t g = (uint8_t) (255 * TVG_RGB16_G(data) / 31); + uint8_t b = (uint8_t) (255 * TVG_RGB16_B(data) / 15); + *out_color = PIXEL_ARGB(0xff, r, g, b); return TVG_SUCCESS; } case TVG_COLOR_U8888: { @@ -424,10 +418,10 @@ static tvg_result_t tvg_read_color(tvg_context_t *ctx, twin_argb32_t *out_color) } } -static float tvg_downscale_coord(tvg_context_t *ctx, uint32_t coord) +/* coord / 2^scale in 16.16 */ +static twin_fixed_t tvg_downscale_coord(tvg_context_t *ctx, uint32_t coord) { - uint16_t factor = (((uint16_t) 1) << ctx->scale); - return (float) coord / (float) factor; + return (twin_fixed_t) (((uint64_t) coord << 16) >> ctx->scale); } static tvg_result_t tvg_read_varuint(tvg_context_t *ctx, uint32_t *out_value) @@ -448,7 +442,7 @@ static tvg_result_t tvg_read_varuint(tvg_context_t *ctx, uint32_t *out_value) return TVG_SUCCESS; } -static tvg_result_t tvg_read_unit(tvg_context_t *ctx, float *out_value) +static tvg_result_t tvg_read_unit(tvg_context_t *ctx, twin_fixed_t *out_value) { uint32_t val; tvg_result_t res = tvg_read_coord(ctx, &val); @@ -459,14 +453,9 @@ static tvg_result_t tvg_read_unit(tvg_context_t *ctx, float *out_value) static tvg_result_t tvg_read_point(tvg_context_t *ctx, tvg_point_t *out_point) { - float f32; - tvg_result_t res = tvg_read_unit(ctx, &f32); - __return_val_if_fail(res, "Failed to read unit"); - out_point->x = f32; - res = tvg_read_unit(ctx, &f32); + tvg_result_t res = tvg_read_unit(ctx, &out_point->x); __return_val_if_fail(res, "Failed to read unit"); - out_point->y = f32; - return TVG_SUCCESS; + return tvg_read_unit(ctx, &out_point->y); } static tvg_result_t tvg_parse_header(tvg_context_t *ctx, int dim_only) @@ -641,18 +630,18 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) { tvg_result_t res = TVG_SUCCESS; tvg_point_t start_point, cur_point, pt; - float f32; + twin_fixed_t f32; uint8_t path_info; twin_path_t *path = ctx->path; res = tvg_read_point(ctx, &pt); __goto_if_fail(res, error, "Failed to read point"); - twin_path_move(path, D(pt.x), D(pt.y)); + twin_path_move(path, pt.x, pt.y); start_point = pt; cur_point = pt; size_t read = 0; for (size_t j = 0; j < size; ++j) { READ_VALUE(read, &path_info, 1); - float line_width = 0.0f; + twin_fixed_t line_width = 0; if (TVG_PATH_CMD_HAS_LINE(path_info)) { res = tvg_read_unit(ctx, &line_width); __goto_if_fail(res, error, "Failed to read unit"); @@ -661,7 +650,7 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) case TVG_PATH_LINE: res = tvg_read_point(ctx, &pt); __goto_if_fail(res, error, "Failed to read point"); - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); cur_point = pt; break; case TVG_PATH_HLINE: @@ -669,7 +658,7 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) __goto_if_fail(res, error, "Failed to read unit"); pt.x = f32; pt.y = cur_point.y; - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); cur_point = pt; break; case TVG_PATH_VLINE: @@ -677,7 +666,7 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) __goto_if_fail(res, error, "Failed to read unit"); pt.x = cur_point.x; pt.y = f32; - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); cur_point = pt; break; case TVG_PATH_CUBIC: { @@ -688,28 +677,27 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) __goto_if_fail(res, error, "Failed to read point"); res = tvg_read_point(ctx, &end_point); __goto_if_fail(res, error, "Failed to read point"); - twin_path_curve(path, D(ctrl_point1.x), D(ctrl_point1.y), - D(ctrl_point2.x), D(ctrl_point2.y), D(end_point.x), - D(end_point.y)); + twin_path_curve(path, ctrl_point1.x, ctrl_point1.y, ctrl_point2.x, + ctrl_point2.y, end_point.x, end_point.y); cur_point = end_point; } break; case TVG_PATH_ARC_CIRCLE: { uint8_t circle_info; READ_VALUE(read, &circle_info, 1); - float radius; + twin_fixed_t radius; res = tvg_read_unit(ctx, &radius); __goto_if_fail(res, error, "Failed to read unit"); res = tvg_read_point(ctx, &pt); __goto_if_fail(res, error, "Failed to read point"); - twin_path_arc_circle( - path, TVG_ARC_LARGE(circle_info), TVG_ARC_SWEEP(circle_info), - D(radius), D(cur_point.x), D(cur_point.y), D(pt.x), D(pt.y)); + twin_path_arc_circle(path, TVG_ARC_LARGE(circle_info), + TVG_ARC_SWEEP(circle_info), radius, + cur_point.x, cur_point.y, pt.x, pt.y); cur_point = pt; } break; case TVG_PATH_ARC_ELLIPSE: { uint8_t ellipse_info; READ_VALUE(read, &ellipse_info, 1); - float radius_x, radius_y, rotation; + twin_fixed_t radius_x, radius_y, rotation; res = tvg_read_unit(ctx, &radius_x); __goto_if_fail(res, error, "Failed to read unit"); res = tvg_read_unit(ctx, &radius_y); @@ -720,12 +708,13 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) __goto_if_fail(res, error, "Failed to read point"); twin_path_arc_ellipse( path, TVG_ARC_LARGE(ellipse_info), TVG_ARC_SWEEP(ellipse_info), - D(radius_x), D(radius_y), D(cur_point.x), D(cur_point.y), - D(pt.x), D(pt.y), rotation * TWIN_ANGLE_360 / 360); + radius_x, radius_y, cur_point.x, cur_point.y, pt.x, pt.y, + (twin_angle_t) ((int64_t) rotation * TWIN_ANGLE_360 / 360 >> + 16)); cur_point = pt; } break; case TVG_PATH_CLOSE: - twin_path_draw(path, D(start_point.x), D(start_point.y)); + twin_path_draw(path, start_point.x, start_point.y); cur_point = start_point; break; case TVG_PATH_QUAD: { @@ -734,8 +723,8 @@ static tvg_result_t tvg_parse_path(tvg_context_t *ctx, size_t size) __goto_if_fail(res, error, "Failed to read point"); res = tvg_read_point(ctx, &end_point); __goto_if_fail(res, error, "Failed to read point"); - twin_path_quadratic_curve(path, D(ctrl_point.x), D(ctrl_point.y), - D(end_point.x), D(end_point.y)); + twin_path_quadratic_curve(path, ctrl_point.x, ctrl_point.y, + end_point.x, end_point.y); cur_point = end_point; } break; default: @@ -752,7 +741,7 @@ static tvg_result_t tvg_parse_rect(tvg_context_t *ctx, tvg_rect_t *out_rect) tvg_point_t pt; tvg_result_t res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); - float w, h; + twin_fixed_t w, h; res = tvg_read_unit(ctx, &w); __return_val_if_fail(res, "Failed to read unit"); res = tvg_read_unit(ctx, &h); @@ -820,7 +809,7 @@ static tvg_result_t tvg_parse_fill_rectangles(tvg_context_t *ctx, while (count--) { res = tvg_parse_rect(ctx, &r); __return_val_if_fail(res, "Failed to parse rect"); - twin_path_rectangle(path, D(r.x), D(r.y), D(r.width), D(r.height)); + twin_path_rectangle(path, r.x, r.y, r.width, r.height); _fill_path_with_style(ctx, fill_style); twin_path_empty(path); } @@ -832,22 +821,22 @@ static tvg_result_t tvg_parse_line_fill_rectangles( size_t size, const tvg_style_t *fill_style, const tvg_style_t *line_style, - float line_width) + twin_fixed_t line_width) { size_t count = size; tvg_result_t res; tvg_rect_t r; if (line_width == 0) { - line_width = .01; + line_width = TVG_HAIRLINE; } twin_path_t *path = ctx->path; while (count--) { res = tvg_parse_rect(ctx, &r); __return_val_if_fail(res, "Failed to parse rect"); - twin_path_rectangle(path, D(r.x), D(r.y), D(r.width), D(r.height)); + twin_path_rectangle(path, r.x, r.y, r.width, r.height); _fill_path_with_style(ctx, fill_style); - _stroke_path_with_style(ctx, line_style, D(line_width)); + _stroke_path_with_style(ctx, line_style, line_width); twin_path_empty(path); } return TVG_SUCCESS; @@ -883,7 +872,7 @@ static tvg_result_t tvg_parse_fill_paths(tvg_context_t *ctx, static tvg_result_t tvg_parse_line_paths(tvg_context_t *ctx, size_t size, const tvg_style_t *line_style, - float line_width) + twin_fixed_t line_width) { tvg_result_t res = TVG_SUCCESS; uint32_t *sizes = twin_malloc(size * sizeof(uint32_t)); @@ -901,7 +890,10 @@ static tvg_result_t tvg_parse_line_paths(tvg_context_t *ctx, res = tvg_parse_path(ctx, sizes[i]); __goto_if_fail(res, error, "Failed to parse path"); } - _stroke_path_with_style(ctx, line_style, D(line_width)); + 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); @@ -912,7 +904,7 @@ static tvg_result_t tvg_parse_line_fill_paths(tvg_context_t *ctx, size_t size, const tvg_style_t *fill_style, const tvg_style_t *line_style, - float line_width) + twin_fixed_t line_width) { tvg_result_t res = TVG_SUCCESS; uint32_t *sizes = twin_malloc(size * sizeof(uint32_t)); @@ -932,10 +924,10 @@ static tvg_result_t tvg_parse_line_fill_paths(tvg_context_t *ctx, __goto_if_fail(res, error, "Failed to parse path"); } if (line_width == 0) { - line_width = .1; + line_width = TVG_THIN_LINE; } _fill_path_with_style(ctx, fill_style); - _stroke_path_with_style(ctx, line_style, D(line_width)); + _stroke_path_with_style(ctx, line_style, line_width); twin_path_empty(path); error: twin_free(sizes); @@ -951,11 +943,11 @@ static tvg_result_t tvg_parse_fill_polygon(tvg_context_t *ctx, tvg_result_t res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); twin_path_t *path = ctx->path; - twin_path_move(path, D(pt.x), D(pt.y)); + twin_path_move(path, pt.x, pt.y); while (--count) { res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); } twin_path_close(path); _fill_path_with_style(ctx, fill_style); @@ -966,26 +958,26 @@ static tvg_result_t tvg_parse_fill_polygon(tvg_context_t *ctx, static tvg_result_t tvg_parse_polyline(tvg_context_t *ctx, size_t size, const tvg_style_t *line_style, - float line_width, + twin_fixed_t line_width, bool close) { tvg_point_t pt; tvg_result_t res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); twin_path_t *path = ctx->path; - twin_path_move(path, D(pt.x), D(pt.y)); + twin_path_move(path, pt.x, pt.y); for (size_t i = 1; i < size; ++i) { res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); } if (close) { twin_path_close(path); } if (line_width == 0) { - line_width = .01; + line_width = TVG_HAIRLINE; } - _stroke_path_with_style(ctx, line_style, D(line_width)); + _stroke_path_with_style(ctx, line_style, line_width); twin_path_empty(path); return TVG_SUCCESS; } @@ -994,13 +986,13 @@ static tvg_result_t tvg_parse_line_fill_polyline(tvg_context_t *ctx, size_t size, const tvg_style_t *fill_style, const tvg_style_t *line_style, - float line_width, + twin_fixed_t line_width, bool close) { tvg_point_t pt; tvg_result_t res = tvg_read_point(ctx, &pt); twin_path_t *path = ctx->path; - twin_path_move(path, D(pt.x), D(pt.y)); + twin_path_move(path, pt.x, pt.y); for (size_t i = 1; i < size; ++i) { res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); @@ -1010,9 +1002,9 @@ static tvg_result_t tvg_parse_line_fill_polyline(tvg_context_t *ctx, } _fill_path_with_style(ctx, fill_style); if (line_width == 0) { - line_width = .01; + line_width = TVG_HAIRLINE; } - _stroke_path_with_style(ctx, line_style, D(line_width)); + _stroke_path_with_style(ctx, line_style, line_width); twin_path_empty(path); return res; } @@ -1020,7 +1012,7 @@ static tvg_result_t tvg_parse_line_fill_polyline(tvg_context_t *ctx, static tvg_result_t tvg_parse_lines(tvg_context_t *ctx, size_t size, const tvg_style_t *line_style, - float line_width) + twin_fixed_t line_width) { tvg_point_t pt; tvg_result_t res; @@ -1028,15 +1020,15 @@ static tvg_result_t tvg_parse_lines(tvg_context_t *ctx, for (size_t i = 0; i < size; ++i) { res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); - twin_path_move(path, D(pt.x), D(pt.y)); + twin_path_move(path, pt.x, pt.y); res = tvg_read_point(ctx, &pt); __return_val_if_fail(res, "Failed to read point"); - twin_path_draw(path, D(pt.x), D(pt.y)); + twin_path_draw(path, pt.x, pt.y); } if (line_width == 0) { - line_width = .01; + line_width = TVG_HAIRLINE; } - _stroke_path_with_style(ctx, line_style, D(line_width)); + _stroke_path_with_style(ctx, line_style, line_width); twin_path_empty(path); return TVG_SUCCESS; } @@ -1207,180 +1199,101 @@ static size_t inp_func(uint8_t *data, size_t to_read, void *state) return fread(data, 1, to_read, f); } -twin_pixmap_t *_twin_tvg_to_pixmap(const char *filepath, twin_format_t fmt) +/* Largest size within @budget bytes, keeping the w:h aspect; false if + * nothing fits */ +static bool tvg_fit_budget(twin_format_t fmt, + uint32_t *w, + uint32_t *h, + size_t budget) { - FILE *infile = NULL; - twin_pixmap_t *pix = NULL; - uint32_t width, height; - tvg_result_t res; - - if (!filepath) { - log_error("Invalid filepath"); - goto bail; - } - - infile = fopen(filepath, "rb"); - if (!infile) { - log_error("Failed to open %s", filepath); - goto bail; - } - - res = tvg_document_dimensions(inp_func, infile, &width, &height); - if (res != TVG_SUCCESS) { - log_error("Failed to get document dimensions"); - goto bail_infile; - } - - if (fseek(infile, 0, SEEK_SET) != 0) { - log_error("Failed to seek file"); - goto bail_infile; - } - - pix = twin_pixmap_create(fmt, width, height); - if (!pix) { - log_error("Failed to create pixmap"); - goto bail_infile; - } - - res = tvg_render_document(inp_func, infile, pix, TWIN_FIXED_ONE, - TWIN_FIXED_ONE); - if (res != TVG_SUCCESS) { - log_error("Failed to render document"); - goto bail_pixmap; + size_t size; + if (tvg_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) { + uint32_t mid_h = lo + (hi - lo) / 2; + 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) { + best_w = mid_w; + best_h = mid_h; + lo = mid_h + 1; + } else { + hi = mid_h - 1; + } } - - fclose(infile); - return pix; - -bail_pixmap: - twin_pixmap_destroy(pix); -bail_infile: - fclose(infile); -bail: - return NULL; + *w = best_w; + *h = best_h; + return best_w && best_h; } -twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath, - twin_format_t fmt, - twin_coord_t w, - twin_coord_t 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 */ +static twin_pixmap_t *tvg_load(const char *filepath, + twin_format_t fmt, + uint32_t w, + uint32_t h) { - FILE *infile = NULL; twin_pixmap_t *pix = NULL; uint32_t width, height; - tvg_result_t res; - - if (!filepath) { - log_error("Invalid filepath"); - goto bail; - } - infile = fopen(filepath, "rb"); + if (!filepath) + return NULL; + FILE *infile = fopen(filepath, "rb"); if (!infile) { log_error("Failed to open %s", filepath); + return NULL; + } + tvg_result_t res = + tvg_document_dimensions(inp_func, infile, &width, &height); + if (res != TVG_SUCCESS || !width || !height || fseek(infile, 0, SEEK_SET)) { + log_error("Failed to read %s", filepath); goto bail; } - - res = tvg_document_dimensions(inp_func, infile, &width, &height); - __goto_if_fail(res, bail_infile, "Failed to get document dimensions"); - - if (fseek(infile, 0, SEEK_SET) != 0) { - log_error("Failed to seek file"); - goto bail_infile; + if (!w || !h) { + w = width; + h = height; } + /* Pixmaps are indexed with twin_coord_t */ + w = MIN(w, INT16_MAX); + h = MIN(h, INT16_MAX); +#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; +#else + (void) tvg_fit_budget; +#endif - pix = twin_pixmap_create(fmt, w, h); + pix = twin_pixmap_create(fmt, (twin_coord_t) w, (twin_coord_t) h); if (!pix) { log_error("Failed to create pixmap"); - goto bail_infile; + goto bail; } - twin_fixed_t width_scale = D((double) w / (double) width); - twin_fixed_t height_scale = D((double) h / (double) height); - twin_fixed_t scale = MIN(width_scale, height_scale); + /* Fit the drawing: the smaller of the two ratios, in 16.16 */ + twin_fixed_t sx = (twin_fixed_t) (((uint64_t) w << 16) / width); + twin_fixed_t sy = (twin_fixed_t) (((uint64_t) h << 16) / height); + twin_fixed_t scale = MIN(sx, sy); res = tvg_render_document(inp_func, infile, pix, scale, scale); - __goto_if_fail(res, bail_pixmap, "Failed to render document"); - + if (res != TVG_SUCCESS) { + log_error("Failed to render document"); + twin_pixmap_destroy(pix); + pix = NULL; + } +bail: fclose(infile); return pix; - -bail_pixmap: - twin_pixmap_destroy(pix); -bail_infile: - fclose(infile); -bail: - return NULL; } -twin_pixmap_t *twin_tvg_to_pixmap_budget(const char *filepath, - twin_format_t fmt, - size_t memory_budget) +twin_pixmap_t *_twin_tvg_to_pixmap(const char *filepath, twin_format_t fmt) { - FILE *infile = NULL; - uint32_t width, height; - - if (!filepath) - return NULL; - - infile = fopen(filepath, "rb"); - if (!infile) - return NULL; - - tvg_result_t res = - tvg_document_dimensions(inp_func, infile, &width, &height); - fclose(infile); - if (res != TVG_SUCCESS) - return NULL; - - /* Compute the largest output dimensions that fit within budget */ - if (width == 0 || height == 0) - return NULL; - size_t full_size; - if (!tvg_pixmap_alloc_size(fmt, width, height, &full_size)) - return NULL; - - /* Clamp to twin_coord_t range (int16_t max = 32767) */ - if (width > 32767) - width = 32767; - if (height > 32767) - height = 32767; - - twin_coord_t out_w, out_h; - if (full_size <= memory_budget) { - out_w = (twin_coord_t) width; - out_h = (twin_coord_t) height; - } else { - if (memory_budget <= sizeof(twin_pixmap_t)) - return NULL; - - uint32_t lo = 1; - uint32_t hi = height; - uint32_t best_w = 0; - uint32_t best_h = 0; - - while (lo <= hi) { - uint32_t mid_h = lo + (hi - lo) / 2; - uint32_t mid_w = ((uint64_t) width * mid_h) / height; - size_t candidate_size; - - if (mid_w < 1) - mid_w = 1; - - if (tvg_pixmap_alloc_size(fmt, mid_w, mid_h, &candidate_size) && - candidate_size <= memory_budget) { - best_w = mid_w; - best_h = mid_h; - lo = mid_h + 1; - } else { - hi = mid_h - 1; - } - } - - if (best_w == 0 || best_h == 0) - return NULL; - - out_w = (twin_coord_t) best_w; - out_h = (twin_coord_t) best_h; - } + return tvg_load(filepath, fmt, 0, 0); +} - return twin_tvg_to_pixmap_scale(filepath, fmt, out_w, out_h); +twin_pixmap_t *twin_tvg_to_pixmap(const char *filepath, + twin_coord_t w, + twin_coord_t h) +{ + return tvg_load(filepath, TWIN_ARGB32, w > 0 ? (uint32_t) w : 0, + h > 0 ? (uint32_t) h : 0); } diff --git a/src/image.c b/src/image.c index 017be4e9..e2ba7ee2 100644 --- a/src/image.c +++ b/src/image.c @@ -129,10 +129,10 @@ static loader_func_t image_loaders[] = { }; /* clang-format on */ -twin_pixmap_t *twin_pixmap_from_file(const char *path, twin_format_t fmt) +twin_pixmap_t *twin_pixmap_from_file(const char *path) { loader_func_t loader = image_loaders[image_type_detect(path)]; if (!loader) return NULL; - return loader(path, fmt); + return loader(path, TWIN_ARGB32); } diff --git a/src/label.c b/src/label.c index 59fe926a..c47a838f 100644 --- a/src/label.c +++ b/src/label.c @@ -85,11 +85,7 @@ twin_dispatch_result_t _twin_label_dispatch(twin_widget_t *widget, return TwinDispatchContinue; } -void twin_label_set(twin_label_t *label, - const char *value, - twin_argb32_t foreground, - twin_fixed_t font_size, - twin_style_t font_style) +void twin_label_set(twin_label_t *label, const char *value) { if (value) { char *new = twin_malloc(strlen(value) + 1); @@ -101,9 +97,6 @@ void twin_label_set(twin_label_t *label, strcpy(label->label, value); } } - label->font_size = font_size; - label->font_style = font_style; - label->foreground = foreground; _twin_widget_queue_layout(&label->widget); } @@ -121,7 +114,10 @@ void _twin_label_init(twin_label_t *label, label->offset.x = 0; label->offset.y = 0; label->align = TwinAlignCenter; - twin_label_set(label, value, foreground, font_size, font_style); + label->font_size = font_size; + label->font_style = font_style; + label->foreground = foreground; + twin_label_set(label, value); } @@ -139,3 +135,14 @@ twin_label_t *twin_label_create(twin_box_t *parent, _twin_label_dispatch); return label; } + +void twin_label_set_align(twin_label_t *label, twin_align_t align) +{ + label->align = align; + _twin_widget_queue_paint(&label->widget); +} + +twin_widget_t *twin_label_widget(twin_label_t *label) +{ + return &label->widget; +} diff --git a/src/log.c b/src/log.c index 56923905..1a910c75 100644 --- a/src/log.c +++ b/src/log.c @@ -24,7 +24,7 @@ #include "log.h" -#define MAX_CALLBACKS 32 +#define MAX_CALLBACKS 4 typedef struct { log_func_t fn; diff --git a/src/matrix.c b/src/matrix.c index 87e6a994..b291515c 100644 --- a/src/matrix.c +++ b/src/matrix.c @@ -71,33 +71,6 @@ void twin_matrix_scale(twin_matrix_t *m, twin_fixed_t sx, twin_fixed_t sy) twin_matrix_multiply(m, &t, m); } -twin_fixed_t _twin_matrix_determinant(twin_matrix_t *matrix) -{ - twin_fixed_t a, b, c, d; - - a = matrix->m[0][0]; - b = matrix->m[0][1]; - c = matrix->m[1][0]; - d = matrix->m[1][1]; - - twin_fixed_t det = twin_fixed_mul(a, d) - twin_fixed_mul(b, c); - - return det; -} - -twin_point_t _twin_matrix_expand(twin_matrix_t *matrix) -{ - twin_fixed_t a = matrix->m[0][0]; - twin_fixed_t d = matrix->m[1][1]; - twin_fixed_t aa = twin_fixed_mul(a, a); - twin_fixed_t dd = twin_fixed_mul(d, d); - twin_point_t expand; - - expand.x = twin_fixed_sqrt(aa + dd); - expand.y = twin_fixed_div(_twin_matrix_determinant(matrix), expand.x); - return expand; -} - void twin_matrix_rotate(twin_matrix_t *m, twin_angle_t a) { twin_fixed_t c, s; @@ -154,28 +127,12 @@ twin_sfixed_t _twin_matrix_dy(twin_matrix_t *m, twin_fixed_t x, twin_fixed_t y) twin_fixed_mul(m->m[1][1], y)); } -twin_sfixed_t _twin_matrix_len(twin_matrix_t *m, - twin_fixed_t dx, - twin_fixed_t dy) -{ - twin_fixed_t xs = - (twin_fixed_mul(m->m[0][0], dx) + twin_fixed_mul(m->m[1][0], dy)); - twin_fixed_t ys = - (twin_fixed_mul(m->m[0][1], dx) + twin_fixed_mul(m->m[1][1], dy)); - twin_fixed_t ds = (twin_fixed_mul(xs, xs) + twin_fixed_mul(ys, ys)); - return (twin_fixed_to_sfixed(twin_fixed_sqrt(ds))); -} - -twin_fixed_t twin_matrix_transform_x(const twin_matrix_t *m, - twin_fixed_t x, - twin_fixed_t y) -{ - return twin_sfixed_to_fixed(_twin_matrix_x((twin_matrix_t *) m, x, y)); -} - -twin_fixed_t twin_matrix_transform_y(const twin_matrix_t *m, - twin_fixed_t x, - twin_fixed_t y) +void twin_matrix_transform(const twin_matrix_t *m, + twin_fixed_t x, + twin_fixed_t y, + twin_fixed_t *ox, + twin_fixed_t *oy) { - return twin_sfixed_to_fixed(_twin_matrix_y((twin_matrix_t *) m, x, y)); + *ox = _twin_matrix_fx((twin_matrix_t *) m, x, y); + *oy = _twin_matrix_fy((twin_matrix_t *) m, x, y); } diff --git a/src/memstats.c b/src/memstats.c index 726a32ae..514aa93d 100644 --- a/src/memstats.c +++ b/src/memstats.c @@ -22,7 +22,7 @@ * are tracked in the table, so the table storage must not come from the * same pool. */ -#define MEMTBL_INLINE_CAPACITY 512 +#define MEMTBL_INLINE_CAPACITY 128 typedef struct { void *ptr; @@ -108,10 +108,23 @@ static size_t memtbl_remove(void *ptr) return sz; } +unsigned int _twin_alloc_fail_next; + +/* Consume one injected failure, if any is pending */ +static bool _injected_failure(void) +{ + if (!_twin_alloc_fail_next) + return false; + _twin_alloc_fail_next--; + return true; +} + void *_twin_malloc(size_t size, const char *file, int line) { (void) file; (void) line; + if (_injected_failure()) + return NULL; void *ptr = twin_raw_malloc(size); if (ptr) { if (!memtbl_insert(ptr, size)) { @@ -134,6 +147,8 @@ void *_twin_calloc(size_t n, size_t size, const char *file, int line) /* Overflow check */ if (n && size > SIZE_MAX / n) return NULL; + if (_injected_failure()) + return NULL; void *ptr = twin_raw_calloc(n, size); if (ptr) { size_t total = n * size; @@ -164,6 +179,10 @@ void *_twin_realloc(void *old, size_t size, const char *file, int line) return NULL; } + /* A failed realloc leaves @old allocated, as realloc() does */ + if (_injected_failure()) + return NULL; + ptrdiff_t old_idx = old ? memtbl_find(old) : -1; if (old_idx < 0 && memtbl_count == memtbl_capacity && !memtbl_grow()) return NULL; diff --git a/src/path.c b/src/path.c index 8b83da6e..78a69134 100644 --- a/src/path.c +++ b/src/path.c @@ -126,14 +126,6 @@ void twin_path_move(twin_path_t *path, twin_fixed_t x, twin_fixed_t y) _twin_matrix_y(&path->state.matrix, x, y)); } -void twin_path_rmove(twin_path_t *path, twin_fixed_t dx, twin_fixed_t dy) -{ - twin_spoint_t here = _twin_path_current_spoint(path); - _twin_path_smove(path, - here.x + _twin_matrix_dx(&path->state.matrix, dx, dy), - here.y + _twin_matrix_dy(&path->state.matrix, dx, dy)); -} - void twin_path_draw(twin_path_t *path, twin_fixed_t x, twin_fixed_t y) { _twin_path_sdraw(path, _twin_matrix_x(&path->state.matrix, x, y), @@ -147,14 +139,6 @@ static void twin_path_draw_polar(twin_path_t *path, twin_angle_t deg) twin_path_draw(path, c, s); } -void twin_path_rdraw(twin_path_t *path, twin_fixed_t dx, twin_fixed_t dy) -{ - twin_spoint_t here = _twin_path_current_spoint(path); - _twin_path_sdraw(path, - here.x + _twin_matrix_dx(&path->state.matrix, dx, dy), - here.y + _twin_matrix_dy(&path->state.matrix, dx, dy)); -} - void twin_path_close(twin_path_t *path) { twin_spoint_t f; @@ -189,10 +173,20 @@ void twin_path_ellipse(twin_path_t *path, twin_path_close(path); } +/* pi sqrt(8) in 16.16: sides per square root of the radius in pixels */ +#define ARC_SIDES_PER_ROOT ((twin_fixed_t) 582338) + +/* An upper bound on how far @m moves a unit vector: the largest + * singular value never exceeds the largest absolute row or column sum + * (it is at most the geometric mean of the two norms). Exact for an + * axis-aligned scale, so an uneven ellipse gets its long radius. */ static twin_fixed_t _twin_matrix_max_radius(twin_matrix_t *m) { - return (twin_fixed_abs(m->m[0][0]) + twin_fixed_abs(m->m[0][1]) + - twin_fixed_abs(m->m[1][0]) + twin_fixed_abs(m->m[1][1])); + twin_fixed_t a = twin_fixed_abs(m->m[0][0]), b = twin_fixed_abs(m->m[0][1]); + twin_fixed_t c = twin_fixed_abs(m->m[1][0]), d = twin_fixed_abs(m->m[1][1]); + twin_fixed_t r = max(a + b, c + d); + r = max(r, a + c); + return max(r, b + d); } void twin_path_arc(twin_path_t *path, @@ -203,13 +197,18 @@ void twin_path_arc(twin_path_t *path, twin_angle_t start, twin_angle_t extent) { - twin_matrix_t save = twin_path_current_matrix(path); + twin_matrix_t save = path->state.matrix; twin_path_translate(path, x, y); twin_path_scale(path, x_radius, y_radius); + /* A chord of an n-gon strays r (1 - cos(pi / n)) ~ r pi^2 / (2 n^2) + * from the circle, so a deviation of at most 1/16 pixel (a quarter of + * the rasterizer's sample spacing) needs n = pi sqrt(8 r) sides. */ twin_fixed_t max_radius = _twin_matrix_max_radius(&path->state.matrix); - int32_t sides = max_radius / twin_sfixed_to_fixed(TWIN_SFIXED_TOLERANCE); + twin_fixed_t root = twin_fixed_sqrt(max_radius); + int32_t sides = + twin_fixed_to_int(twin_fixed_mul(root, ARC_SIDES_PER_ROOT)) + 1; if (sides > 1024) sides = 1024; @@ -363,7 +362,7 @@ void twin_path_arc_ellipse(twin_path_t *path, twin_ellipse_param_t param; param = get_center_parameters(cur_x, cur_y, target_x, target_y, large_arc, sweep, radius_x, radius_y, rotation); - twin_matrix_t save = twin_path_current_matrix(path); + twin_matrix_t save = path->state.matrix; twin_path_translate(path, param.cx, param.cy); twin_path_rotate(path, rotation); @@ -408,7 +407,7 @@ void twin_path_rounded_rectangle(twin_path_t *path, twin_fixed_t x_radius, twin_fixed_t y_radius) { - twin_matrix_t save = twin_path_current_matrix(path); + twin_matrix_t save = path->state.matrix; twin_path_translate(path, x, y); twin_path_move(path, 0, y_radius); @@ -442,44 +441,11 @@ void twin_path_lozenge(twin_path_t *path, twin_path_rounded_rectangle(path, x, y, w, h, radius, radius); } -void twin_path_tab(twin_path_t *path, - twin_fixed_t x, - twin_fixed_t y, - twin_fixed_t w, - twin_fixed_t h, - twin_fixed_t x_radius, - twin_fixed_t y_radius) -{ - twin_matrix_t save = twin_path_current_matrix(path); - - twin_path_translate(path, x, y); - twin_path_move(path, 0, y_radius); - twin_path_arc(path, x_radius, y_radius, x_radius, y_radius, TWIN_ANGLE_180, - TWIN_ANGLE_90); - twin_path_draw(path, w - x_radius, 0); - twin_path_arc(path, w - x_radius, y_radius, x_radius, y_radius, - TWIN_ANGLE_270, TWIN_ANGLE_90); - twin_path_draw(path, w, h); - twin_path_draw(path, 0, h); - twin_path_close(path); - twin_path_set_matrix(path, save); -} - void twin_path_set_matrix(twin_path_t *path, twin_matrix_t matrix) { path->state.matrix = matrix; } -twin_matrix_t twin_path_current_matrix(twin_path_t *path) -{ - return path->state.matrix; -} - -void twin_path_identity(twin_path_t *path) -{ - twin_matrix_identity(&path->state.matrix); -} - void twin_path_translate(twin_path_t *path, twin_fixed_t tx, twin_fixed_t ty) { twin_matrix_translate(&path->state.matrix, tx, ty); @@ -500,31 +466,16 @@ void twin_path_set_font_size(twin_path_t *path, twin_fixed_t font_size) path->state.font_size = font_size; } -twin_fixed_t twin_path_current_font_size(twin_path_t *path) -{ - return path->state.font_size; -} - void twin_path_set_font_style(twin_path_t *path, twin_style_t font_style) { path->state.font_style = font_style; } -twin_style_t twin_path_current_font_style(twin_path_t *path) -{ - return path->state.font_style; -} - void twin_path_set_cap_style(twin_path_t *path, twin_cap_t cap_style) { path->state.cap_style = cap_style; } -twin_cap_t twin_path_current_cap_style(twin_path_t *path) -{ - return path->state.cap_style; -} - void twin_path_empty(twin_path_t *path) { path->npoints = 0; @@ -535,11 +486,17 @@ static void _twin_path_reset_state(twin_path_t *path) { twin_path_empty(path); twin_matrix_identity(&path->state.matrix); + path->state.font = NULL; path->state.font_size = TWIN_FIXED_ONE * 15; path->state.font_style = TwinStyleRoman; path->state.cap_style = TwinCapRound; } +void twin_path_set_font(twin_path_t *path, twin_font_t *font) +{ + path->state.font = font; +} + twin_path_t *_twin_path_acquire(twin_screen_t *screen) { if (screen && screen->path_cache_count > 0) { @@ -579,14 +536,20 @@ void _twin_path_release(twin_screen_t *screen, twin_path_t *path) } void twin_path_bounds(twin_path_t *path, twin_rect_t *rect) +{ + _twin_path_bounds_from(path, 0, rect); +} + +void _twin_path_bounds_from(const twin_path_t *path, + int first, + twin_rect_t *rect) { twin_sfixed_t left = TWIN_SFIXED_MAX; twin_sfixed_t top = TWIN_SFIXED_MAX; twin_sfixed_t right = TWIN_SFIXED_MIN; twin_sfixed_t bottom = TWIN_SFIXED_MIN; - int i; - for (i = 0; i < path->npoints; i++) { + for (int i = first; i < path->npoints; i++) { twin_sfixed_t x = path->points[i].x; twin_sfixed_t y = path->points[i].y; if (x < left) @@ -627,35 +590,237 @@ void twin_path_restore(twin_path_t *path, twin_state_t *state) path->state = *state; } -twin_path_t *twin_path_create(void) +void _twin_path_init(twin_path_t *path) { - twin_path_t *path; - - path = twin_malloc(sizeof(twin_path_t)); - if (!path) - return NULL; - path->npoints = 0; - path->nsublen = 0; path->points = path->inline_points; path->size_points = TWIN_PATH_INLINE_POINTS; path->sublen = path->inline_sublen; path->size_sublen = TWIN_PATH_INLINE_SUBLEN; - twin_matrix_identity(&path->state.matrix); - path->state.font_size = TWIN_FIXED_ONE * 15; - path->state.font_style = TwinStyleRoman; - path->state.cap_style = TwinCapRound; - return path; + _twin_path_reset_state(path); } -void twin_path_destroy(twin_path_t *path) +void _twin_path_fini(twin_path_t *path) { if (path->points != path->inline_points) twin_free(path->points); if (path->sublen != path->inline_sublen) twin_free(path->sublen); +} + +twin_path_t *twin_path_create(void) +{ + twin_path_t *path = twin_malloc(sizeof(twin_path_t)); + if (path) + _twin_path_init(path); + return path; +} + +void twin_path_destroy(twin_path_t *path) +{ + _twin_path_fini(path); twin_free(path); } +/* + * Coverage for @path on @dst. + * + * On return @mask describes A8 coverage whose (0, 0) sits at (*mx, *my) in + * path coordinates, and @vis is the part of the path bounds inside the + * destination clip. Small shapes come from (or go into) the coverage cache + * as full-bounds masks; large ones are rasterized only where visible. + * Returns false when nothing is visible or memory ran out. + */ +typedef struct { + twin_pixmap_t hdr; /* mask pixmap header (no allocation) */ + twin_pixmap_t *heap; /* heap mask to release, if any */ + bool scratch; /* mask lives in the scratch arena */ + size_t scratch_saved; +} twin_mask_t; + +static uint64_t _twin_path_key(const twin_path_t *path, const twin_rect_t *b) +{ + uint64_t h = TWIN_HASH_INIT ^ 0x50; /* 'P': path domain */ + twin_sfixed_t ox = twin_int_to_sfixed(b->left); + twin_sfixed_t oy = twin_int_to_sfixed(b->top); + for (int i = 0; i < path->npoints; i++) { + twin_sfixed_t xy[2] = {(twin_sfixed_t) (path->points[i].x - ox), + (twin_sfixed_t) (path->points[i].y - oy)}; + h = _twin_hash(h, xy, sizeof(xy)); + } + h = _twin_hash(h, path->sublen, sizeof(int) * path->nsublen); + h = _twin_hash(h, &path->npoints, sizeof(path->npoints)); + return h; +} + +static bool _twin_path_mask(twin_pixmap_t *dst, + twin_path_t *path, + twin_scratch_t *scratch, + twin_mask_t *m, + twin_coord_t *mx, + twin_coord_t *my, + twin_rect_t *vis, + bool *banded) +{ + twin_rect_t b; + twin_path_bounds(path, &b); + if (b.left >= b.right || b.top >= b.bottom) + return false; + + twin_rect_t c = twin_pixmap_get_clip(dst); /* in path coordinates */ + *vis = (twin_rect_t) {max(b.left, c.left), min(b.right, c.right), + max(b.top, c.top), min(b.bottom, c.bottom)}; + if (vis->left >= vis->right || vis->top >= vis->bottom) + return false; + + m->heap = NULL; + m->scratch = false; + twin_coord_t w = b.right - b.left, h = b.bottom - b.top; + size_t full = (size_t) _twin_a8_stride(w) * (size_t) h; + + if (full <= CONFIG_CACHE_MAX_ENTRY && _twin_cache_enabled()) { + uint64_t key = _twin_path_key(path, &b); + twin_cache_entry_t *e = _twin_cache_lookup(key); + bool fresh = false, collide = false; + /* A 64-bit hash collision must never hand back a mask of another + * size: that would read outside it. Nor may it insert a duplicate + * key, which lookups could never reach: rasterize uncached. */ + if (e && (e->width != w || e->height != h)) { + e = NULL; + collide = true; + } + if (!e && !collide) { + e = _twin_cache_insert(key, w, h); + fresh = e != NULL; + } + if (e) { + _twin_pixmap_init_a8(&m->hdr, e->width, e->height, e->stride, + e->data); + if (fresh) + twin_fill_path(&m->hdr, path, -b.left, -b.top, scratch); + *mx = b.left; + *my = b.top; + return true; + } + } + + /* Rasterize only the visible part */ + w = vis->right - vis->left; + h = vis->bottom - vis->top; + twin_coord_t stride = _twin_a8_stride(w); + size_t bytes = (size_t) stride * (size_t) h; + void *pixels = NULL; + if (scratch) { + m->scratch_saved = twin_scratch_save(scratch); + pixels = twin_scratch_alloc(scratch, bytes, 4); + m->scratch = pixels != NULL; + } + if (pixels) { + memset(pixels, 0, bytes); + _twin_pixmap_init_a8(&m->hdr, w, h, stride, pixels); + } else if (banded) { + /* Larger than the scratch arena: the caller rasterizes in bands */ + *banded = true; + return false; + } else { + /* Larger than the scratch arena: a heap mask for this call only, so + * an idle screen holds no mask memory */ + twin_pixmap_t *mask = twin_pixmap_create(TWIN_A8, w, h); + if (!mask) + return false; + m->heap = mask; + for (twin_coord_t y = 0; y < h; y++) + memset(twin_pixmap_pointer(mask, 0, y).v, 0, (size_t) w); + _twin_pixmap_init_a8(&m->hdr, w, h, mask->stride, mask->p.v); + } + twin_fill_path(&m->hdr, path, -vis->left, -vis->top, scratch); + *mx = vis->left; + *my = vis->top; + return true; +} + +static void _twin_mask_release(twin_mask_t *m, twin_scratch_t *scratch) +{ + twin_pixmap_reset_xform_cache(&m->hdr); + if (m->scratch) + twin_scratch_restore(scratch, m->scratch_saved); + if (m->heap) + twin_pixmap_destroy(m->heap); +} + +/* Bytes of mask a band may take: half the scratch arena, leaving room for + * the edges, or a small heap buffer without one */ +#define TWIN_BAND_HEAP_BYTES 4096 + +/* + * Composite @path over the visible rectangle @vis one band of rows at a + * time, so a shape larger than the scratch arena needs a band-sized mask + * rather than a heap mask of its full size. Edges that start above a band + * are stepped to it exactly, so the coverage matches a single pass. + */ +typedef struct { + twin_pixmap_t *dst, *band; + twin_operand_t *src; + twin_coord_t src_x, src_y; + twin_operator_t operator; + twin_rect_t vis; +} _band_composite_t; + +static void _band_composite(void *ctx, twin_coord_t top, twin_coord_t rows) +{ + _band_composite_t *c = ctx; + twin_coord_t y = c->vis.top + top; + twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = c->band}; + twin_composite(c->dst, c->vis.left, y, c->src, c->src_x + c->vis.left, + c->src_y + y, &msk, 0, 0, c->operator, + c->vis.right - c->vis.left, rows); + twin_pixmap_reset_xform_cache(c->band); +} + +/* + * Composite @path over the visible rectangle @vis one band of rows at a + * time, so a shape larger than the scratch arena needs a band-sized mask + * rather than a heap mask of its full size. The edges are built and + * sorted once; the scan hands each band over as it leaves it. + */ +static void _twin_composite_path_bands(twin_pixmap_t *dst, + twin_operand_t *src, + twin_coord_t src_x, + twin_coord_t src_y, + twin_path_t *path, + twin_operator_t operator, + twin_scratch_t * scratch, + twin_rect_t vis) +{ + twin_coord_t w = vis.right - vis.left, h = vis.bottom - vis.top; + twin_coord_t stride = _twin_a8_stride(w); + size_t budget = scratch ? scratch->size / 2 : TWIN_BAND_HEAP_BYTES; + twin_coord_t rows = (twin_coord_t) (budget / (size_t) stride); + if (rows < 1) + rows = 1; + if (rows > h) + rows = h; + size_t bytes = (size_t) stride * (size_t) rows; + + size_t saved = scratch ? twin_scratch_save(scratch) : 0; + uint8_t *buf = scratch ? twin_scratch_alloc(scratch, bytes, 4) : NULL; + uint8_t *heap = NULL; + if (!buf) { + heap = twin_malloc(bytes); + if (!heap) + return; + buf = heap; + } + memset(buf, 0, bytes); + twin_pixmap_t band; + _twin_pixmap_init_a8(&band, w, rows, stride, buf); + _band_composite_t c = {dst, &band, src, src_x, src_y, operator, vis }; + twin_band_t bands = {h, _band_composite, &c}; + twin_fill_path_banded(&band, &bands, path, -vis.left, -vis.top, scratch); + if (scratch) + twin_scratch_restore(scratch, saved); + twin_free(heap); +} + void twin_composite_path(twin_pixmap_t *dst, twin_operand_t *src, twin_coord_t src_x, @@ -664,111 +829,224 @@ void twin_composite_path(twin_pixmap_t *dst, twin_operator_t operator, twin_scratch_t * scratch) { - twin_rect_t bounds; - twin_path_bounds(path, &bounds); - if (bounds.left >= bounds.right || bounds.top >= bounds.bottom) + twin_mask_t m; + twin_coord_t mx, my; + twin_rect_t vis; + bool banded = false; + + if (!_twin_path_mask(dst, path, scratch, &m, &mx, &my, &vis, &banded)) { + if (banded) + _twin_composite_path_bands(dst, src, src_x, src_y, path, operator, + scratch, vis); return; + } + twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = &m.hdr}; + twin_composite(dst, vis.left, vis.top, src, src_x + vis.left, + src_y + vis.top, &msk, vis.left - mx, vis.top - my, operator, + vis.right - vis.left, vis.bottom - vis.top); + _twin_mask_release(&m, scratch); +} + +/* The screen's scratch arena for drawing on @dst, or NULL off screen */ +static twin_scratch_t *_twin_screen_scratch(twin_pixmap_t *dst, + twin_scratch_t *s) +{ + if (!dst->screen || !dst->screen->scratch_buf) + return NULL; + twin_scratch_init(s, dst->screen->scratch_buf, dst->screen->scratch_size); + return s; +} - twin_coord_t width = bounds.right - bounds.left; - twin_coord_t height = bounds.bottom - bounds.top; +static twin_argb32_t _twin_lerp_argb(twin_argb32_t c0, + twin_argb32_t c1, + twin_fixed_t t) +{ + if (t <= 0) + return c0; + if (t >= TWIN_FIXED_ONE) + return c1; + /* |b - a| * t < 2^24: 32-bit products suffice */ + twin_argb32_t out = 0; + for (int shift = 0; shift < 32; shift += 8) { + int32_t a = (c0 >> shift) & 0xff, b = (c1 >> shift) & 0xff; + int32_t v = a + (((b - a) * t + 0x8000) >> 16); + out |= (twin_argb32_t) (v & 0xff) << shift; + } + return out; +} - /* Try allocating mask pixmap from scratch arena */ - size_t s_stride = (size_t) width; /* TWIN_A8: 1 byte per pixel */ - if (s_stride & 3) - s_stride = (s_stride + 3) & ~3; - size_t mask_size = sizeof(twin_pixmap_t) + s_stride * (size_t) height; - twin_pixmap_t *mask = NULL; - bool mask_from_scratch = false; - size_t saved = 0; +/* Floor division stepped by a constant: q + r / d advances by dq + dr / d + * per step, with 0 <= r, dr < d */ +typedef struct { + int64_t q, r, dq, dr, d; +} _twin_step_t; - if (scratch) { - saved = twin_scratch_save(scratch); - mask = twin_scratch_alloc(scratch, mask_size, _Alignof(twin_pixmap_t)); +static void _twin_floor_divmod(int64_t n, int64_t d, int64_t *q, int64_t *r) +{ + *q = n / d; + *r = n % d; + if (*r < 0) { + *r += d; + (*q)--; } - if (mask) { - mask_from_scratch = true; - memset(mask, 0, mask_size); - mask->screen = 0; - mask->up = 0; - mask->down = 0; - mask->x = mask->y = 0; - mask->format = TWIN_A8; - mask->width = width; - mask->height = height; - twin_matrix_identity(&mask->transform); - mask->clip.left = mask->clip.top = 0; - mask->clip.right = width; - mask->clip.bottom = height; - mask->origin_x = mask->origin_y = 0; - mask->stride = (twin_coord_t) s_stride; - mask->disable = 0; - mask->animation = NULL; -#if defined(CONFIG_DROP_SHADOW) - mask->shadow = false; -#endif - mask->window = NULL; - mask->xform_cache = NULL; - mask->xform_cache_size = 0; - mask->p.v = mask + 1; - } else { - /* Try the screen's reusable mask cache before a fresh alloc. - * The cache grows to the high-water mark and persists across - * composite calls, eliminating repeated malloc/free. */ - twin_screen_t *screen = dst->screen; - if (screen && screen->mask_cache && screen->mask_cache_width >= width && - screen->mask_cache_height >= height) { - mask = screen->mask_cache; - } else if (screen) { - if (screen->mask_cache) - twin_pixmap_destroy(screen->mask_cache); - mask = twin_pixmap_create(TWIN_A8, width, height); - screen->mask_cache = mask; - screen->mask_cache_width = mask ? width : 0; - screen->mask_cache_height = mask ? height : 0; - } else { - mask = twin_pixmap_create(TWIN_A8, width, height); - } - if (!mask) - return; - /* Reset mask state for this composite operation */ - memset(mask->p.v, 0, (size_t) mask->stride * height); - mask->clip.left = mask->clip.top = 0; - mask->clip.right = width; - mask->clip.bottom = height; - mask->origin_x = mask->origin_y = 0; - twin_matrix_identity(&mask->transform); +} + +/* Start at @n / @d, stepping by @step / @d; @d > 0 */ +static _twin_step_t _twin_step_init(int64_t n, int64_t step, int64_t d) +{ + _twin_step_t t = {.d = d}; + _twin_floor_divmod(n, d, &t.q, &t.r); + _twin_floor_divmod(step, d, &t.dq, &t.dr); + return t; +} + +static inline void _twin_step(_twin_step_t *t) +{ + t->q += t->dq; + t->r += t->dr; + if (t->r >= t->d) { + t->r -= t->d; + t->q++; } +} - twin_fill_path(mask, path, -bounds.left, -bounds.top, scratch); - twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = mask}; - twin_composite(dst, bounds.left, bounds.top, src, src_x + bounds.left, - src_y + bounds.top, &msk, 0, 0, operator, width, height); +/* Gradient color at 16.16 position @q, clamped to the ends */ +static inline twin_argb32_t _twin_lerp_q(twin_argb32_t c0, + twin_argb32_t c1, + int64_t q) +{ + return q <= 0 ? c0 + : q >= TWIN_FIXED_ONE ? c1 + : _twin_lerp_argb(c0, c1, (twin_fixed_t) q); +} - if (mask_from_scratch) { - twin_pixmap_reset_xform_cache(mask); - twin_scratch_restore(scratch, saved); +void twin_paint_path_linear(twin_pixmap_t *dst, + twin_path_t *path, + twin_fixed_t x0, + twin_fixed_t y0, + twin_argb32_t c0, + twin_fixed_t x1, + twin_fixed_t y1, + twin_argb32_t c1) +{ + twin_scratch_t scratch; + twin_scratch_t *sp = _twin_screen_scratch(dst, &scratch); + + twin_mask_t m; + twin_coord_t mx, my; + twin_rect_t vis; + if (!_twin_path_mask(dst, path, sp, &m, &mx, &my, &vis, NULL)) + return; + + twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = &m.hdr}; + twin_coord_t w = vis.right - vis.left; + int64_t gx = (int64_t) x1 - x0, gy = (int64_t) y1 - y0; + int64_t len2 = (gx * gx + gy * gy) >> 16; /* 16.16 */ + + if (gx == 0 || len2 == 0) { + /* Vertical (or degenerate): one solid color per scanline. t is + * (cy - y0) / gy; a row down adds one pixel to cy, so t steps by a + * fixed quotient and remainder. With the signs folded into a + * positive divisor, floor and truncation agree wherever t > 0. */ + int64_t d = gy < 0 ? -gy : gy; + int64_t sign = gy < 0 ? -1 : 1; + _twin_step_t t = {0, 0, 0, 0, d ? d : 1}; + if (gy) { + twin_fixed_t cy = twin_int_to_fixed(vis.top) + TWIN_FIXED_HALF; + t = _twin_step_init(sign * ((int64_t) cy - y0) * 65536, + sign * ((int64_t) TWIN_FIXED_ONE << 16), d); + } + for (twin_coord_t y = vis.top; y < vis.bottom; y++) { + twin_operand_t src = {.source_kind = TWIN_SOLID, + .u.argb = _twin_lerp_q(c0, c1, t.q)}; + twin_composite(dst, vis.left, y, &src, 0, 0, &msk, vis.left - mx, + y - my, TWIN_OVER, w, 1); + _twin_step(&t); /* steps by nothing when gy is 0 */ + } + _twin_mask_release(&m, sp); + return; } - /* Heap-allocated masks are either in the screen cache (kept) - * or allocated without a screen (freed). */ - if (!mask_from_scratch && - (!dst->screen || mask != dst->screen->mask_cache)) { - twin_pixmap_destroy(mask); + + /* General direction: build each row's colors, then composite it */ + twin_argb32_t *row = twin_malloc(sizeof(twin_argb32_t) * (size_t) w); + if (!row) { + _twin_mask_release(&m, sp); + return; + } + twin_pixmap_t span; + _twin_pixmap_init(&span, TWIN_ARGB32, w, 1, (twin_coord_t) (w * 4), row); + twin_operand_t src = {.source_kind = TWIN_PIXMAP, .u.pixmap = &span}; + + /* t = dot * 65536 / len2, floored. One pixel to the right adds + * exactly gx to dot and one row down adds exactly gy, so t is stepped + * as a quotient and remainder both ways: no 64-bit division per row or + * pixel on 32-bit targets. Floor and truncating division differ only + * for t <= 0, which selects c0 either way. */ + twin_fixed_t cx = twin_int_to_fixed(vis.left) + TWIN_FIXED_HALF; + twin_fixed_t cy = twin_int_to_fixed(vis.top) + TWIN_FIXED_HALF; + int64_t dot = (((int64_t) cx - x0) * gx + ((int64_t) cy - y0) * gy) >> 16; + _twin_step_t row_t = _twin_step_init(dot * 65536, gy * 65536, len2); + _twin_step_t px_t = _twin_step_init(0, gx * 65536, len2); + for (twin_coord_t y = vis.top; y < vis.bottom; y++) { + if (y == vis.top || gy) { + _twin_step_t t = row_t; + t.dq = px_t.dq; + t.dr = px_t.dr; + for (twin_coord_t i = 0; i < w; i++) { + row[i] = _twin_lerp_q(c0, c1, t.q); + _twin_step(&t); + } + _twin_step(&row_t); + } + twin_composite(dst, vis.left, y, &src, 0, 0, &msk, vis.left - mx, + y - my, TWIN_OVER, w, 1); } + twin_pixmap_reset_xform_cache(&span); + twin_free(row); + _twin_mask_release(&m, sp); } void twin_paint_path(twin_pixmap_t *dst, twin_argb32_t argb, twin_path_t *path) { twin_operand_t src = {.source_kind = TWIN_SOLID, .u.argb = argb}; twin_scratch_t scratch; - twin_scratch_t *sp = NULL; - if (dst->screen && dst->screen->scratch_buf) { - twin_scratch_init(&scratch, dst->screen->scratch_buf, - dst->screen->scratch_size); - sp = &scratch; - } + twin_scratch_t *sp = _twin_screen_scratch(dst, &scratch); twin_composite_path(dst, &src, 0, 0, path, TWIN_OVER, sp); } +/* + * True when nothing of @stroke drawn with @pen can reach the clip, so the + * convolution can be skipped. The extent is taken from the points directly + * because twin_path_bounds() reports flat lines as empty. Twice the pen's + * reach covers square caps and miters. + */ +static bool _twin_stroke_misses_clip(twin_pixmap_t *dst, + const twin_path_t *stroke, + const twin_path_t *pen) +{ + if (!stroke->npoints) + return true; + int reach = 0; + for (int i = 0; i < pen->npoints; i++) { + int ax = abs(pen->points[i].x), ay = abs(pen->points[i].y); + reach = max(reach, max(ax, ay)); + } + twin_sfixed_t l = stroke->points[0].x, r = l; + twin_sfixed_t t = stroke->points[0].y, b = t; + for (int i = 1; i < stroke->npoints; i++) { + l = min(l, stroke->points[i].x); + r = max(r, stroke->points[i].x); + t = min(t, stroke->points[i].y); + b = max(b, stroke->points[i].y); + } + twin_coord_t grow = twin_sfixed_trunc(twin_sfixed_ceil(2 * reach)) + 2; + twin_rect_t clip = twin_pixmap_get_clip(dst); + return twin_sfixed_trunc(l) - grow >= clip.right || + twin_sfixed_trunc(twin_sfixed_ceil(r)) + grow <= clip.left || + twin_sfixed_trunc(t) - grow >= clip.bottom || + twin_sfixed_trunc(twin_sfixed_ceil(b)) + grow <= clip.top; +} + void twin_composite_stroke(twin_pixmap_t *dst, twin_operand_t *src, twin_coord_t src_x, @@ -786,13 +1064,19 @@ void twin_composite_stroke(twin_pixmap_t *dst, _twin_path_release(screen, pen); return; } - twin_matrix_t m = twin_path_current_matrix(stroke); + twin_matrix_t m = stroke->state.matrix; m.m[2][0] = 0; m.m[2][1] = 0; twin_path_set_matrix(pen, m); - twin_path_set_cap_style(path, twin_path_current_cap_style(stroke)); + twin_path_set_cap_style(path, stroke->state.cap_style); twin_path_circle(pen, 0, 0, pen_width / 2); + + if (_twin_stroke_misses_clip(dst, stroke, pen)) { + _twin_path_release(screen, path); + _twin_path_release(screen, pen); + return; + } twin_path_convolve(path, stroke, pen, scratch); twin_composite_path(dst, src, src_x, src_y, path, operator, scratch); _twin_path_release(screen, path); @@ -806,11 +1090,6 @@ void twin_paint_stroke(twin_pixmap_t *dst, { twin_operand_t src = {.source_kind = TWIN_SOLID, .u.argb = argb}; twin_scratch_t scratch; - twin_scratch_t *sp = NULL; - if (dst->screen && dst->screen->scratch_buf) { - twin_scratch_init(&scratch, dst->screen->scratch_buf, - dst->screen->scratch_size); - sp = &scratch; - } + twin_scratch_t *sp = _twin_screen_scratch(dst, &scratch); twin_composite_stroke(dst, &src, 0, 0, stroke, pen_width, TWIN_OVER, sp); } diff --git a/src/pixmap.c b/src/pixmap.c index 489777a9..f359d5ae 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -9,11 +9,6 @@ #include "twin_private.h" -#if defined(CONFIG_WINDOW_MANAGER) -#define TWIN_BW 0 -#define TWIN_TITLE_HEIGHT 20 -#endif - #define IS_ALIGNED(p, alignment) ((p % alignment) == 0) #define ALIGN_UP(sz, alignment) \ (((alignment) & ((alignment) - 1)) == 0 \ @@ -45,6 +40,29 @@ static bool twin_pixmap_alloc_size(twin_format_t format, return true; } +void _twin_pixmap_init(twin_pixmap_t *pixmap, + twin_format_t format, + twin_coord_t width, + twin_coord_t height, + twin_coord_t stride, + void *pixels) +{ + memset(pixmap, 0, sizeof(*pixmap)); + pixmap->format = format; + pixmap->width = width; + pixmap->height = height; + pixmap->stride = stride; + twin_matrix_identity(&pixmap->transform); + pixmap->clip.right = width; + pixmap->clip.bottom = height; + pixmap->scan_height = height; + pixmap->opacity = 0xff; + /* 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) @@ -53,88 +71,38 @@ twin_pixmap_t *twin_pixmap_create(twin_format_t format, if (!twin_pixmap_alloc_size(format, width, height, &size)) return NULL; - /* Compute stride in size_t to avoid int16_t overflow. */ - size_t stride_sz = - ALIGN_UP((size_t) twin_bytes_per_pixel(format) * (size_t) width, 4); - size_t space = stride_sz * (size_t) height; - - /* Stride must fit in twin_coord_t for the pixmap struct field. */ - if (stride_sz > INT16_MAX) - return NULL; - twin_coord_t stride = (twin_coord_t) stride_sz; + twin_coord_t stride = (twin_coord_t) ALIGN_UP( + (size_t) twin_bytes_per_pixel(format) * width, 4); twin_pixmap_t *pixmap = twin_malloc(size); if (!pixmap) return NULL; - - pixmap->screen = 0; - pixmap->up = 0; - pixmap->down = 0; - pixmap->x = pixmap->y = 0; - pixmap->format = format; - pixmap->width = width; - pixmap->height = height; - twin_matrix_identity(&pixmap->transform); - pixmap->clip.left = pixmap->clip.top = 0; - pixmap->clip.right = pixmap->width; - pixmap->clip.bottom = pixmap->height; - pixmap->origin_x = pixmap->origin_y = 0; - pixmap->stride = stride; - pixmap->disable = 0; - pixmap->animation = NULL; - pixmap->shadow = false; - pixmap->window = NULL; /* Initialize window field */ - pixmap->xform_cache = NULL; - pixmap->xform_cache_size = 0; - pixmap->p.v = pixmap + 1; - memset(pixmap->p.v, '\0', space); + _twin_pixmap_init(pixmap, format, width, height, stride, pixmap + 1); + memset(pixmap->p.v, 0, size - sizeof(twin_pixmap_t)); return pixmap; } -twin_pixmap_t *twin_pixmap_create_budget(twin_format_t format, - twin_coord_t max_width, - twin_coord_t max_height, - size_t memory_budget) +twin_pixmap_t *_twin_pixmap_create_stub(twin_format_t format, + twin_coord_t width, + twin_coord_t height) { - if (max_width <= 0 || max_height <= 0) - return NULL; - - size_t full_size; - if (twin_pixmap_alloc_size(format, max_width, max_height, &full_size) && - full_size <= memory_budget) - return twin_pixmap_create(format, max_width, max_height); - - if (memory_budget <= sizeof(twin_pixmap_t)) - return NULL; - - twin_coord_t lo = 1; - twin_coord_t hi = max_height; - twin_coord_t best_w = 0; - twin_coord_t best_h = 0; - - while (lo <= hi) { - twin_coord_t mid_h = lo + (hi - lo) / 2; - twin_coord_t mid_w = - (twin_coord_t) (((int32_t) max_width * mid_h) / max_height); - size_t candidate_size; - - if (mid_w < 1) - mid_w = 1; - - if (twin_pixmap_alloc_size(format, mid_w, mid_h, &candidate_size) && - candidate_size <= memory_budget) { - best_w = mid_w; - best_h = mid_h; - lo = mid_h + 1; - } else { - hi = mid_h - 1; - } - } - - if (best_w == 0 || best_h == 0) + twin_pixmap_t *pixmap = twin_malloc(sizeof(twin_pixmap_t)); + if (!pixmap) return NULL; + _twin_pixmap_init(pixmap, format, width, height, 0, NULL); + /* An empty clip turns every drawing operation into a no-op. */ + pixmap->clip = (twin_rect_t) {0, 0, 0, 0}; + pixmap->opaque_rect = (twin_rect_t) {0, 0, 0, 0}; + return pixmap; +} - return twin_pixmap_create(format, best_w, best_h); +void _twin_pixmap_init_a8(twin_pixmap_t *pixmap, + twin_coord_t width, + twin_coord_t height, + twin_coord_t stride, + void *pixels) +{ + _twin_pixmap_init(pixmap, TWIN_A8, width, height, stride, pixels); } twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, @@ -146,25 +114,7 @@ twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, twin_pixmap_t *pixmap = twin_malloc(sizeof(twin_pixmap_t)); if (!pixmap) return NULL; - - pixmap->screen = 0; - pixmap->up = 0; - pixmap->down = 0; - pixmap->x = pixmap->y = 0; - pixmap->format = format; - pixmap->width = width; - pixmap->height = height; - twin_matrix_identity(&pixmap->transform); - pixmap->clip.left = pixmap->clip.top = 0; - pixmap->clip.right = pixmap->width; - pixmap->clip.bottom = pixmap->height; - pixmap->origin_x = pixmap->origin_y = 0; - pixmap->stride = stride; - pixmap->disable = 0; - pixmap->window = NULL; /* Initialize window field */ - pixmap->xform_cache = NULL; - pixmap->xform_cache_size = 0; - pixmap->p = pixels; + _twin_pixmap_init(pixmap, format, width, height, stride, pixels.v); return pixmap; } @@ -173,6 +123,7 @@ void twin_pixmap_destroy(twin_pixmap_t *pixmap) if (pixmap->screen) twin_pixmap_hide(pixmap); twin_pixmap_reset_xform_cache(pixmap); + _twin_animation_destroy(pixmap->animation); twin_free(pixmap); } @@ -186,6 +137,14 @@ void twin_pixmap_reset_xform_cache(twin_pixmap_t *pixmap) pixmap->xform_cache_size = 0; } +void _twin_pixmap_damage_extents(twin_pixmap_t *pixmap) +{ + if (!pixmap->screen) + return; + twin_rect_t e = _twin_pixmap_extents(pixmap); + twin_screen_damage(pixmap->screen, e.left, e.top, e.right, e.bottom); +} + void twin_pixmap_show(twin_pixmap_t *pixmap, twin_screen_t *screen, twin_pixmap_t *lower) @@ -193,11 +152,15 @@ void twin_pixmap_show(twin_pixmap_t *pixmap, if (pixmap->disable) twin_screen_disable_update(screen); + /* Linking above a pixmap shown elsewhere (or nowhere) would corrupt + * the display list: stack on top instead. */ + if (lower && lower->screen != screen) + lower = screen->top; if (lower == pixmap) lower = pixmap->down; if (pixmap->screen) - twin_pixmap_hide(pixmap); + _twin_pixmap_unlink(pixmap); pixmap->screen = screen; @@ -207,18 +170,22 @@ void twin_pixmap_show(twin_pixmap_t *pixmap, lower->up = pixmap; if (!pixmap->up) screen->top = pixmap; + else + pixmap->up->down = pixmap; } else { pixmap->down = NULL; pixmap->up = screen->bottom; + if (screen->bottom) + screen->bottom->down = pixmap; screen->bottom = pixmap; if (!pixmap->up) screen->top = pixmap; } - twin_pixmap_damage(pixmap, 0, 0, pixmap->width, pixmap->height); + _twin_pixmap_damage_extents(pixmap); } -void twin_pixmap_hide(twin_pixmap_t *pixmap) +void _twin_pixmap_unlink(twin_pixmap_t *pixmap) { twin_screen_t *screen = pixmap->screen; twin_pixmap_t **up, **down; @@ -226,7 +193,7 @@ void twin_pixmap_hide(twin_pixmap_t *pixmap) if (!screen) return; - twin_pixmap_damage(pixmap, 0, 0, pixmap->width, pixmap->height); + _twin_pixmap_damage_extents(pixmap); if (pixmap->up) down = &pixmap->up->down; @@ -248,15 +215,71 @@ void twin_pixmap_hide(twin_pixmap_t *pixmap) twin_screen_enable_update(screen); } -twin_pointer_t twin_pixmap_pointer(twin_pixmap_t *pixmap, - twin_coord_t x, - twin_coord_t y) +void twin_pixmap_hide(twin_pixmap_t *pixmap) { - twin_pointer_t p; + twin_screen_t *screen = pixmap->screen; + if (!screen) + return; + _twin_pixmap_unlink(pixmap); + /* Never leave the screen pointing at a pixmap it no longer shows. */ + _twin_screen_forget(screen, pixmap); +} - p.b = (pixmap->p.b + (int32_t) y * pixmap->stride + - (int32_t) x * twin_bytes_per_pixel(pixmap->format)); - return p; +void _twin_screen_forget(twin_screen_t *screen, const twin_pixmap_t *pixmap) +{ + if (screen->active == pixmap) + screen->active = NULL; + if (screen->target == pixmap) { + screen->target = NULL; + screen->clicklock = false; + } +} + +void _twin_pixmap_set_scan_height(twin_pixmap_t *pixmap, twin_coord_t rows) +{ + if (rows < 0) + rows = 0; + if (rows > pixmap->height) + rows = pixmap->height; + if (pixmap->scan_height == rows) + return; + /* Damage both the old and the new extent */ + _twin_pixmap_damage_extents(pixmap); + pixmap->scan_height = rows; + _twin_pixmap_damage_extents(pixmap); +} + +void twin_pixmap_set_opacity(twin_pixmap_t *pixmap, twin_a8_t opacity) +{ + if (pixmap->opacity == opacity) + return; + pixmap->opacity = opacity; + _twin_pixmap_damage_extents(pixmap); +} + +void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, + twin_coord_t dx, + twin_coord_t dy, + twin_coord_t blur, + twin_a8_t alpha) +{ +#if !defined(CONFIG_DROP_SHADOW) + /* Built without shadow support */ + (void) pixmap, (void) dx, (void) dy, (void) blur, (void) alpha; +#else + twin_shadow_t s = {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))) + return; + _twin_pixmap_damage_extents(pixmap); + pixmap->shadow = s; + _twin_pixmap_damage_extents(pixmap); +#endif } void twin_pixmap_enable_update(twin_pixmap_t *pixmap) @@ -283,14 +306,6 @@ void twin_pixmap_set_origin(twin_pixmap_t *pixmap, pixmap->origin_y = oy; } -void twin_pixmap_offset(twin_pixmap_t *pixmap, - twin_coord_t offx, - twin_coord_t offy) -{ - pixmap->origin_x += offx; - pixmap->origin_y += offy; -} - void twin_pixmap_get_origin(twin_pixmap_t *pixmap, twin_coord_t *ox, twin_coord_t *oy) @@ -365,11 +380,17 @@ twin_rect_t twin_pixmap_save_clip(twin_pixmap_t *pixmap) void twin_pixmap_restore_clip(twin_pixmap_t *pixmap, twin_rect_t rect) { - pixmap->clip = rect; + pixmap->clip = + _twin_pixmap_is_stub(pixmap) ? (twin_rect_t) {0, 0, 0, 0} : rect; } void twin_pixmap_reset_clip(twin_pixmap_t *pixmap) { + /* A stub has no storage: keep its clip empty so drawing is a no-op. */ + if (_twin_pixmap_is_stub(pixmap)) { + pixmap->clip = (twin_rect_t) {0, 0, 0, 0}; + return; + } pixmap->clip.left = 0; pixmap->clip.top = 0; pixmap->clip.right = pixmap->width; @@ -387,64 +408,54 @@ void twin_pixmap_damage(twin_pixmap_t *pixmap, right + pixmap->x, bottom + pixmap->y); } -static twin_argb32_t _twin_pixmap_fetch(twin_pixmap_t *pixmap, - twin_coord_t x, - twin_coord_t y) -{ - twin_pointer_t p = - twin_pixmap_pointer(pixmap, x - pixmap->x, y - pixmap->y); - /* FIXME: for transform */ - - if (pixmap->x <= x && x < pixmap->x + pixmap->width && pixmap->y <= y && - y < pixmap->y + pixmap->height) { - switch (pixmap->format) { - case TWIN_A8: - return *p.a8 << 24; - case TWIN_RGB16: - return twin_rgb16_to_argb32(*p.rgb16); - case TWIN_ARGB32: - return *p.argb32; - } - } - return 0; +void twin_pixmap_move(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y) +{ + if (pixmap->x == x && pixmap->y == y) + return; + _twin_pixmap_damage_extents(pixmap); + pixmap->x = x; + pixmap->y = y; + _twin_pixmap_damage_extents(pixmap); } -bool twin_pixmap_transparent(twin_pixmap_t *pixmap, - twin_coord_t x, - twin_coord_t y) +bool twin_pixmap_dispatch(twin_pixmap_t *pixmap, twin_event_t *event) { - return (_twin_pixmap_fetch(pixmap, x, y) >> 24) == 0; + if (pixmap->window) + return _twin_window_dispatch(pixmap->window, event); + return false; } -bool twin_pixmap_is_iconified(twin_pixmap_t *pixmap, twin_coord_t y) +twin_coord_t twin_pixmap_width(const twin_pixmap_t *pixmap) { -#if defined(CONFIG_WINDOW_MANAGER) - /* - * Check whether the specified area within the pixmap corresponds to an - * iconified window. - */ - if (pixmap->window && - (pixmap->window->iconify && - y >= pixmap->y + TWIN_BW + TWIN_TITLE_HEIGHT + TWIN_BW)) - return true; -#else - (void) y; - (void) pixmap; -#endif - return false; + return pixmap->width; } -void twin_pixmap_move(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y) +twin_coord_t twin_pixmap_height(const twin_pixmap_t *pixmap) { - twin_pixmap_damage(pixmap, 0, 0, pixmap->width, pixmap->height); - pixmap->x = x; - pixmap->y = y; - twin_pixmap_damage(pixmap, 0, 0, pixmap->width, pixmap->height); + return pixmap->height; } -bool twin_pixmap_dispatch(twin_pixmap_t *pixmap, twin_event_t *event) +twin_format_t twin_pixmap_format(const twin_pixmap_t *pixmap) { - if (pixmap->window) - return _twin_window_dispatch(pixmap->window, event); - return false; + return pixmap->format; +} + +void *twin_pixmap_pixels(twin_pixmap_t *pixmap, twin_coord_t *stride) +{ + if (stride) + *stride = pixmap->stride; + return pixmap->p.v; +} + +twin_animation_t *twin_pixmap_animation(const twin_pixmap_t *pixmap) +{ + return pixmap->animation; +} + +void twin_pixmap_set_transform(twin_pixmap_t *pixmap, const twin_matrix_t *m) +{ + if (m) + pixmap->transform = *m; + else + twin_matrix_identity(&pixmap->transform); } diff --git a/src/poly.c b/src/poly.c index f8f2b84c..6b9e15f0 100644 --- a/src/poly.c +++ b/src/poly.c @@ -6,17 +6,21 @@ */ #include +#include #include "twin_private.h" typedef struct _twin_edge { - struct _twin_edge *next; twin_sfixed_t top, bot; twin_sfixed_t x; twin_sfixed_t e; twin_sfixed_t dx, dy; twin_sfixed_t inc_x; twin_sfixed_t step_x; + /* One sample row (TWIN_POLY_STEP) in Bresenham terms: x moves by + * row_x, e by row_e, plus inc_x once e reaches dy */ + twin_sfixed_t row_x; + twin_sfixed_t row_e; int winding; } twin_edge_t; @@ -31,12 +35,30 @@ typedef struct _twin_edge { static int _edge_compare_y(const void *a, const void *b) { - const twin_edge_t *ae = a; - const twin_edge_t *be = 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); } +/* Order @order (pointers into the edge array) by top. Outlines arrive in + * long runs already in order, where insertion sort is near linear; only + * big paths go to qsort, which then moves pointers, not edges. */ +static void _edge_sort(twin_edge_t **order, int n) +{ + if (n > 48) { + qsort(order, n, sizeof(*order), _edge_compare_y); + return; + } + for (int i = 1; i < n; i++) { + twin_edge_t *e = order[i]; + int j = i; + for (; j > 0 && order[j - 1]->top > e->top; j--) + order[j] = order[j - 1]; + order[j] = e; + } +} + static void _edge_step_by(twin_edge_t *edge, twin_sfixed_t dy) { twin_dfixed_t e; @@ -46,6 +68,19 @@ static void _edge_step_by(twin_edge_t *edge, twin_sfixed_t dy) edge->e = e % edge->dy; } +/* _edge_step_by(edge, TWIN_POLY_STEP) without dividing: e < dy and + * row_e < dy, so a single correction keeps e in range */ +static inline void _edge_step_row(twin_edge_t *edge) +{ + twin_dfixed_t e = edge->e + edge->row_e; + edge->x += edge->row_x; + if (e >= edge->dy) { + e -= edge->dy; + edge->x += edge->inc_x; + } + edge->e = e; +} + /* * Returns the nearest grid coordinate no less than f * @@ -108,6 +143,10 @@ static int _twin_edge_build(twin_spoint_t *vertices, } edges[e].step_x = edges[e].inc_x * (edges[e].dx / edges[e].dy); edges[e].dx = edges[e].dx % edges[e].dy; + twin_dfixed_t row = (twin_dfixed_t) TWIN_POLY_STEP * edges[e].dx; + edges[e].row_x = edges[e].step_x * TWIN_POLY_STEP + + edges[e].inc_x * (row / edges[e].dy); + edges[e].row_e = row % edges[e].dy; edges[e].top = vertices[tv].y + dy; edges[e].bot = vertices[bv].y + dy; @@ -124,6 +163,28 @@ static int _twin_edge_build(twin_spoint_t *vertices, return e; } +/* Add coverage @w (at most 0x40) to @n bytes, saturating at 0xff: four + * bytes per load and store on the aligned middle part */ +static void _span_add(twin_a8_t *s, int n, uint32_t w) +{ + for (; n && ((uintptr_t) s & 3); n--, s++) { + twin_a16_t a = *s + w; + *s = twin_sat(a); + } + uint32_t ww = w | w << 16; + for (; n >= 4; n -= 4, s += 4) { + uint32_t v; + memcpy(&v, s, sizeof(v)); + v = _twin_sat_lanes((v & TWIN_LANES) + ww) | + _twin_sat_lanes(((v >> 8) & TWIN_LANES) + ww) << 8; + memcpy(s, &v, sizeof(v)); + } + for (; n; n--, s++) { + twin_a16_t a = *s + w; + *s = twin_sat(a); + } +} + static void _span_fill(twin_pixmap_t *pixmap, twin_sfixed_t y, twin_sfixed_t left, @@ -207,10 +268,11 @@ static void _span_fill(twin_pixmap_t *pixmap, w += cover[col]; /* middle pixels */ - while (x + TWIN_POLY_MASK < right) { - a = *s + w; - *s++ = twin_sat(a); - x += TWIN_POLY_SAMPLE; + if (x + TWIN_POLY_MASK < right) { + int n = (right - x) >> TWIN_POLY_SHIFT; + _span_add(s, n, w); + s += n; + x += n * TWIN_POLY_SAMPLE; } /* last pixel */ @@ -226,90 +288,129 @@ static void _span_fill(twin_pixmap_t *pixmap, } } +/* Insert @e into @aet[0..n), ordered by x, after any edge at the same x */ +static inline void _aet_insert(twin_edge_t **aet, int n, twin_edge_t *e) +{ + int j = n; + for (; j > 0 && aet[j - 1]->x > e->x; j--) + aet[j] = aet[j - 1]; + aet[j] = e; +} + +/* Scan the edges top to bottom. The active edges live in an array kept + * ordered by x: new edges are inserted in place, finished ones compacted + * out, and after each step one stable insertion pass restores the order, + * which costs a move per crossing rather than a rescan per crossing. */ +/* Banding state: the band holds mask rows [top, top + rows) */ +typedef struct { + const twin_band_t *b; + twin_pixmap_t *pixmap; + twin_coord_t top, rows; +} _band_state_t; + +/* Hand the current band over and clear it for the next rows */ +static void _band_next(_band_state_t *st) +{ + twin_coord_t rows = min(st->rows, (twin_coord_t) (st->b->total - st->top)); + st->b->flush(st->b->ctx, st->top, rows); + for (twin_coord_t r = 0; r < rows; r++) + memset(st->pixmap->p.a8 + r * st->pixmap->stride, 0, + (size_t) st->pixmap->width); + st->top += st->rows; +} + static void _twin_edge_fill(twin_pixmap_t *pixmap, - twin_edge_t *edges, - int nedges) + twin_edge_t **order, + twin_edge_t **aet, + int nedges, + _band_state_t *band) { - twin_edge_t *active, *a, *n, **prev; twin_sfixed_t x0 = 0; + twin_coord_t bottom = band ? band->b->total : pixmap->clip.bottom; if (nedges <= 0) return; - qsort(edges, nedges, sizeof(twin_edge_t), _edge_compare_y); - int e = 0; - twin_sfixed_t y = edges[0].top; - active = 0; + _edge_sort(order, nedges); + int e = 0, na = 0; + twin_sfixed_t y = order[0]->top; + /* Every edge starts below the pixmap */ + if (twin_sfixed_trunc(y) >= bottom) + return; for (;;) { /* add in new edges */ - for (; e < nedges && edges[e].top <= y; e++) { - for (prev = &active; (a = *prev); prev = &(a->next)) - if (a->x > edges[e].x) - break; - edges[e].next = *prev; - *prev = &edges[e]; + for (; e < nedges && order[e]->top <= y; e++) + _aet_insert(aet, na++, order[e]); + + /* move the band down to this row */ + twin_sfixed_t by = y; + if (band) { + while (twin_sfixed_trunc(y) >= band->top + band->rows) + _band_next(band); + by = y - twin_int_to_sfixed(band->top); } /* walk this y value marking coverage */ int w = 0; - for (a = active; a; a = a->next) { + for (int i = 0; i < na; i++) { + twin_edge_t *a = aet[i]; if (w == 0) x0 = a->x; w += a->winding; if (w == 0) - _span_fill(pixmap, y, x0, a->x); + _span_fill(pixmap, by, x0, a->x); } /* step down, clipping to pixmap */ y += TWIN_POLY_STEP; - if (twin_sfixed_trunc(y) >= pixmap->clip.bottom) + if (twin_sfixed_trunc(y) >= bottom) break; /* strip out dead edges */ - for (prev = &active; (a = *prev);) { - if (a->bot <= y) - *prev = a->next; - else - prev = &a->next; - } + int k = 0; + for (int i = 0; i < na; i++) + if (aet[i]->bot > y) + aet[k++] = aet[i]; + na = k; /* check for all done */ - if (!active && e == nedges) + if (!na && e == nedges) break; - /* step all edges */ - for (a = active; a; a = a->next) - _edge_step_by(a, TWIN_POLY_STEP); - - /* fix x sorting */ - for (prev = &active; (a = *prev) && (n = a->next);) { - if (a->x > n->x) { - a->next = n->next; - n->next = a; - *prev = n; - prev = &active; - } else - prev = &a->next; - } + /* step all edges, then fix x sorting */ + for (int i = 0; i < na; i++) + _edge_step_row(aet[i]); + for (int i = 1; i < na; i++) + if (aet[i - 1]->x > aet[i]->x) + _aet_insert(aet, i, aet[i]); } } -void twin_fill_path(twin_pixmap_t *pixmap, - twin_path_t *path, - twin_coord_t dx, - twin_coord_t dy, - twin_scratch_t *scratch) +static void _twin_fill(twin_pixmap_t *pixmap, + twin_path_t *path, + twin_coord_t dx, + twin_coord_t dy, + twin_scratch_t *scratch, + _band_state_t *band) { twin_sfixed_t sdx = twin_int_to_sfixed(dx + pixmap->origin_x); twin_sfixed_t sdy = twin_int_to_sfixed(dy + pixmap->origin_y); int nalloc = path->npoints + path->nsublen + 1; - size_t edge_bytes = sizeof(twin_edge_t) * nalloc; + /* The edges, then two pointers per edge (sort order and active list), + * starting on a pointer boundary */ + size_t ptr_off = + (sizeof(twin_edge_t) * nalloc + _Alignof(twin_edge_t *) - 1) & + ~(_Alignof(twin_edge_t *) - 1); + size_t edge_bytes = ptr_off + 2 * sizeof(twin_edge_t *) * nalloc; bool from_scratch = false; twin_edge_t *edges = NULL; if (scratch) - edges = twin_scratch_alloc(scratch, edge_bytes, _Alignof(twin_edge_t)); + edges = twin_scratch_alloc(scratch, edge_bytes, + _Alignof(twin_edge_t) > _Alignof(void *) + ? _Alignof(twin_edge_t) + : _Alignof(void *)); if (edges) { from_scratch = true; } else { @@ -335,7 +436,33 @@ void twin_fill_path(twin_pixmap_t *pixmap, nedges += n; } } - _twin_edge_fill(pixmap, edges, nedges); + 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); if (!from_scratch) twin_free(edges); } + +void twin_fill_path(twin_pixmap_t *pixmap, + twin_path_t *path, + twin_coord_t dx, + twin_coord_t dy, + twin_scratch_t *scratch) +{ + _twin_fill(pixmap, path, dx, dy, scratch, NULL); +} + +void twin_fill_path_banded(twin_pixmap_t *band, + const twin_band_t *bands, + twin_path_t *path, + twin_coord_t dx, + twin_coord_t dy, + twin_scratch_t *scratch) +{ + _band_state_t st = {bands, band, 0, band->height}; + _twin_fill(band, path, dx, dy, scratch, &st); + /* The rest, including bands the scan never reached */ + while (st.top < bands->total) + _band_next(&st); +} diff --git a/src/screen-ops.c b/src/screen-ops.c index 33c3ef45..036e9549 100644 --- a/src/screen-ops.c +++ b/src/screen-ops.c @@ -11,32 +11,10 @@ #include "twin_private.h" /* - * Porter-Duff "over" operator for ARGB32 pixels - * dst = src + dst * (1 - alpha_src) + * The built-in renderer generates these three span operations along with the + * rest of its compositing table; other renderers (Pixman) get them here. */ -static inline twin_argb32_t over(twin_argb32_t dst, twin_argb32_t src) -{ - uint16_t t1, t2, t3, t4; - twin_a8_t a; - - if (!src) - return dst; - a = ~(src >> 24); - switch (a) { - case 0: - return src; - case 0xff: - dst = (twin_add(src, dst, 0, t1) | twin_add(src, dst, 8, t2) | - twin_add(src, dst, 16, t3) | twin_add(src, dst, 24, t4)); - break; - default: - dst = (twin_over(src, dst, 0, a, t1) | twin_over(src, dst, 8, a, t2) | - twin_over(src, dst, 16, a, t3) | twin_over(src, dst, 24, a, t4)); - break; - } - return dst; -} - +#if !defined(CONFIG_RENDERER_BUILTIN) /* * Convert RGB16 to ARGB32 and copy to destination * Used for compositing RGB16 pixmaps onto ARGB32 screen buffer @@ -45,7 +23,9 @@ void _twin_rgb16_source_argb32(twin_pointer_t dst, twin_source_u src, int width) { twin_argb32_t src32; while (width--) { - src32 = twin_rgb16_to_argb32(*src.p.rgb16++); + /* twin_rgb16_to_argb32() evaluates its argument several times */ + twin_rgb16_t v = *src.p.rgb16++; + src32 = twin_rgb16_to_argb32(v); *dst.argb32++ = src32; } } @@ -61,7 +41,7 @@ void _twin_argb32_over_argb32(twin_pointer_t dst, twin_source_u src, int width) while (width--) { dst32 = *dst.argb32; src32 = *src.p.argb32++; - dst32 = over(dst32, src32); + dst32 = _twin_over(dst32, src32); *dst.argb32++ = dst32; } } @@ -80,3 +60,107 @@ void _twin_argb32_source_argb32(twin_pointer_t dst, *dst.argb32++ = src32; } } +#endif /* !CONFIG_RENDERER_BUILTIN */ + +/* src scaled by a constant alpha */ +static inline twin_argb32_t in_alpha(twin_argb32_t src, twin_a8_t alpha) +{ + return _twin_mul_lanes(src & TWIN_LANES, alpha) | + _twin_mul_lanes((src >> 8) & TWIN_LANES, alpha) << 8; +} + +void _twin_screen_blend_argb32(twin_argb32_t *dst, + const twin_argb32_t *src, + twin_a8_t alpha, + int width) +{ + while (width--) { + *dst = _twin_over(*dst, in_alpha(*src++, alpha)); + dst++; + } +} + +void _twin_screen_blend_rgb16(twin_argb32_t *dst, + const twin_rgb16_t *src, + twin_a8_t alpha, + int width) +{ + while (width--) { + twin_rgb16_t v = *src++; + twin_argb32_t s = twin_rgb16_to_argb32(v); + *dst = _twin_over(*dst, in_alpha(s, alpha)); + dst++; + } +} + +void _twin_screen_fill(twin_argb32_t *dst, twin_argb32_t color, int width) +{ + while (width--) + *dst++ = color; +} + +/* over(under, in_alpha(src, alpha)) for opaque src: the source's alpha + * after scaling is exactly alpha, so each channel is + * mul(c, alpha) + mul(under_c, 255 - alpha). Both terms are bounded by + * their factors, so the sum never exceeds 255 and the under term packs + * into one constant add. Alpha holds for a whole frame of a transition, + * so the products come from a table built once per alpha. */ +static struct { + bool valid; + twin_a8_t alpha; + uint8_t mul[256]; +} _onto_table; + +static const uint8_t *_onto_mul(twin_a8_t alpha) +{ + if (!_onto_table.valid || _onto_table.alpha != alpha) { + for (uint32_t c = 0; c < 256; c++) + _onto_table.mul[c] = (uint8_t) _twin_mul_lanes(c, alpha); + _onto_table.alpha = alpha; + _onto_table.valid = true; + } + return _onto_table.mul; +} + +static inline uint32_t _onto_under(twin_a8_t alpha, twin_argb32_t under) +{ + uint32_t k = 0xff - alpha; + return _twin_mul_lanes(under & TWIN_LANES, k) | + _twin_mul_lanes((under >> 8) & TWIN_LANES, k) << 8; +} + +static inline twin_argb32_t _onto(twin_argb32_t s, + const uint8_t *mul, + uint32_t under) +{ + return ((uint32_t) mul[s >> 24] << 24 | + (uint32_t) mul[(s >> 16) & 0xff] << 16 | + (uint32_t) mul[(s >> 8) & 0xff] << 8 | mul[s & 0xff]) + + under; +} + +void _twin_screen_blend_onto_argb32(twin_argb32_t *dst, + const twin_argb32_t *src, + twin_a8_t alpha, + twin_argb32_t under, + int width) +{ + const uint8_t *mul = _onto_mul(alpha); + uint32_t u = _onto_under(alpha, under); + while (width--) + *dst++ = _onto(*src++, mul, u); +} + +void _twin_screen_blend_onto_rgb16(twin_argb32_t *dst, + const twin_rgb16_t *src, + twin_a8_t alpha, + twin_argb32_t under, + int width) +{ + const uint8_t *mul = _onto_mul(alpha); + uint32_t u = _onto_under(alpha, under); + while (width--) { + twin_rgb16_t v = *src++; + *dst++ = _onto(twin_rgb16_to_argb32(v), mul, u); + } +} diff --git a/src/screen.c b/src/screen.c index 10af945f..f6477c07 100644 --- a/src/screen.c +++ b/src/screen.c @@ -1,88 +1,104 @@ /* * Twin - A Tiny Window System * Copyright (c) 2004 Keith Packard - * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan + * Copyright (c) 2024-2026 National Cheng Kung University, Taiwan * All rights reserved. */ +/* + * Screen compositor + * + * Each window keeps its pixels in a pixmap; the screen composes those + * pixmaps scanline by scanline into a single span buffer and hands each + * span to the backend. There is no screen-sized buffer. + * + * Three things keep this cheap without graphics hardware: + * + * - Damage is a short list of rectangles. Two rectangles merge only when + * their union covers no more pixels than the pair, so a clock tick in the + * status bar does not drag the whole content area into the next frame. + * + * - Occlusion culling. Walking the layers top-down, each fully opaque + * layer adds its extent on the current row to a small set of covered + * intervals. Every layer below is composited only where it is still + * visible; the background only fills what nothing covers. A full-screen + * app on top of other apps therefore costs one copy per pixel. + * + * - 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. + */ + #include +#include #include "twin_private.h" +unsigned _twin_debug; + +#define TWIN_SHADOW_MAX_BLUR 63 + twin_screen_t *twin_screen_create(twin_coord_t width, twin_coord_t height, twin_put_begin_t put_begin, twin_put_span_t put_span, void *closure) { - static bool closure_tracker_initialized = false; - twin_screen_t *screen = twin_calloc(1, sizeof(twin_screen_t)); + /* The window manager's state lives right after the screen */ + twin_screen_t *screen = + twin_calloc(1, sizeof(twin_screen_t) + _twin_wm_state_size); if (!screen) return NULL; - /* Initialize closure tracking system on first screen creation */ - if (!closure_tracker_initialized) { - _twin_closure_tracker_init(); - closure_tracker_initialized = true; - } - - screen->top = 0; - screen->bottom = 0; screen->width = width; screen->height = height; - screen->damage.left = screen->damage.right = 0; - screen->damage.top = screen->damage.bottom = 0; - screen->damaged = NULL; - screen->damaged_closure = NULL; - screen->disable = 0; - screen->background = 0; + screen->background_color = 0xffffffff; screen->put_begin = put_begin; screen->put_span = put_span; screen->closure = closure; - screen->button_x = screen->button_y = -1; - screen->span_cache = NULL; - screen->span_cache_width = 0; - -#ifndef TWIN_SCRATCH_SIZE -#define TWIN_SCRATCH_SIZE (32 * 1024) +#ifndef CONFIG_SCRATCH_SIZE +#define CONFIG_SCRATCH_SIZE 16 #endif - screen->scratch_buf = twin_malloc(TWIN_SCRATCH_SIZE); - screen->scratch_size = screen->scratch_buf ? TWIN_SCRATCH_SIZE : 0; + if (CONFIG_SCRATCH_SIZE > 0) { + screen->scratch_buf = twin_malloc(CONFIG_SCRATCH_SIZE * 1024); + if (screen->scratch_buf) + screen->scratch_size = CONFIG_SCRATCH_SIZE * 1024; + } + _twin_wm_init(screen); + /* Nothing has been shown yet: the first update paints everything */ + twin_screen_damage(screen, 0, 0, width, height); return screen; } void twin_screen_destroy(twin_screen_t *screen) { + /* Let the policy finish animations and drop pixmaps it owns; the + * windows then go without it */ + screen->closing = true; + _twin_wm_fini(screen); + + while (screen->windows) + twin_window_destroy(screen->windows); while (screen->bottom) twin_pixmap_hide(screen->bottom); + if (screen->background) + twin_pixmap_destroy(screen->background); + /* The screen owns its cursor (twin_create() installs it) */ + if (screen->cursor) + twin_pixmap_destroy(screen->cursor); for (int i = 0; i < screen->path_cache_count; i++) twin_path_destroy(screen->path_cache[i]); - if (screen->mask_cache) - twin_pixmap_destroy(screen->mask_cache); twin_free(screen->span_cache); twin_free(screen->scratch_buf); twin_free(screen); } -void twin_screen_register_damaged(twin_screen_t *screen, - void (*damaged)(void *), - void *closure) -{ - screen->damaged = damaged; - screen->damaged_closure = closure; -} - void twin_screen_enable_update(twin_screen_t *screen) { - if (--screen->disable == 0) { - if (screen->damage.left < screen->damage.right && - screen->damage.top < screen->damage.bottom) { - if (screen->damaged) - (*screen->damaged)(screen->damaged_closure); - } - } + --screen->disable; } void twin_screen_disable_update(twin_screen_t *screen) @@ -90,6 +106,68 @@ void twin_screen_disable_update(twin_screen_t *screen) screen->disable++; } +static inline twin_area_t _rect_area(const twin_rect_t *r) +{ + return (twin_area_t) (r->right - r->left) * (r->bottom - r->top); +} + +static inline twin_rect_t _rect_union(const twin_rect_t *a, + const twin_rect_t *b) +{ + twin_rect_t u = { + .left = min(a->left, b->left), + .right = max(a->right, b->right), + .top = min(a->top, b->top), + .bottom = max(a->bottom, b->bottom), + }; + return u; +} + +static inline bool _rect_contains(const twin_rect_t *outer, + const twin_rect_t *inner) +{ + return outer->left <= inner->left && inner->right <= outer->right && + outer->top <= inner->top && inner->bottom <= outer->bottom; +} + +void _twin_rect_list_add(twin_rect_t *list, int *n, int max, twin_rect_t r) +{ + for (;;) { + bool merged = false; + for (int i = 0; i < *n; i++) { + twin_rect_t *d = &list[i]; + if (_rect_contains(d, &r)) + return; + twin_rect_t u = _rect_union(d, &r); + if (_rect_area(&u) <= _rect_area(d) + _rect_area(&r)) { + /* Merged: re-insert so it can absorb further neighbors */ + r = u; + list[i] = list[--*n]; + merged = true; + break; + } + } + if (merged) + continue; + if (*n < max) { + list[(*n)++] = r; + return; + } + int best = 0; + twin_area_t best_cost = 0; + for (int i = 0; i < *n; i++) { + twin_rect_t u = _rect_union(&list[i], &r); + twin_area_t cost = _rect_area(&u) - _rect_area(&list[i]); + if (i == 0 || cost < best_cost) { + best = i; + best_cost = cost; + } + } + r = _rect_union(&list[best], &r); + list[best] = list[--*n]; + } +} + void twin_screen_damage(twin_screen_t *screen, twin_coord_t left, twin_coord_t top, @@ -104,179 +182,648 @@ void twin_screen_damage(twin_screen_t *screen, right = screen->width; if (bottom > screen->height) bottom = screen->height; + if (left >= right || top >= bottom) + return; - if (screen->damage.left == screen->damage.right) { - screen->damage.left = left; - screen->damage.right = right; - screen->damage.top = top; - screen->damage.bottom = bottom; - } else { - if (left < screen->damage.left) - screen->damage.left = left; - if (top < screen->damage.top) - screen->damage.top = top; - if (screen->damage.right < right) - screen->damage.right = right; - if (screen->damage.bottom < bottom) - screen->damage.bottom = bottom; - } - if (screen->damaged && !screen->disable) - (*screen->damaged)(screen->damaged_closure); + _twin_rect_list_add(screen->damage, &screen->n_damage, TWIN_DAMAGE_RECTS, + (twin_rect_t) {left, right, top, bottom}); } void twin_screen_resize(twin_screen_t *screen, twin_coord_t width, twin_coord_t height) { + if (width == screen->width && height == screen->height) + return; screen->width = width; screen->height = height; + screen->n_damage = 0; + twin_screen_disable_update(screen); + /* Rotation or a resized host window: the policy re-places everything */ + _twin_screen_relayout(screen); twin_screen_damage(screen, 0, 0, screen->width, screen->height); + twin_screen_enable_update(screen); } bool twin_screen_damaged(twin_screen_t *screen) { - return (screen->damage.left < screen->damage.right && - screen->damage.top < screen->damage.bottom); + return screen->n_damage > 0; } -static void twin_screen_span_pixmap(twin_screen_t maybe_unused *screen, - twin_argb32_t *span, - twin_pixmap_t *p, - twin_coord_t y, - twin_coord_t left, - twin_coord_t right, - twin_src_op op16, - twin_src_op op32) -{ - twin_pointer_t dst; - twin_source_u src; - twin_coord_t p_left, p_right; +/* + * Interval sets for occlusion: sorted, disjoint, half-open [l, r). + * Capacity overflow only makes culling more conservative, never wrong. + */ +#define TWIN_SPAN_SET_MAX 8 + +typedef struct { + twin_coord_t l, r; +} twin_span_t; - /* bounds check in y */ - if (y < p->y) +typedef struct { + int n; + twin_span_t s[TWIN_SPAN_SET_MAX]; +} twin_span_set_t; + +static void _span_set_add(twin_span_set_t *set, twin_coord_t l, twin_coord_t r) +{ + if (l >= r) return; - if (p->y + p->height <= y) + /* Intervals [i, j) touch [l, r): they merge into one at i */ + int i = 0, n = set->n; + while (i < n && set->s[i].r < l) + i++; + int j = i; + for (; j < n && set->s[j].l <= r; j++) { + l = min(l, set->s[j].l); + r = max(r, set->s[j].r); + } + int o = n - (j - i) + 1; + if (o > TWIN_SPAN_SET_MAX) + return; /* keep the old (smaller) set: conservative */ + if (j != i + 1) { + if (j > i + 1) + for (int k = j; k < n; k++) + set->s[k - (j - i - 1)] = set->s[k]; + else + for (int k = n; k > i; k--) + set->s[k] = set->s[k - 1]; + } + set->s[i] = (twin_span_t) {l, r}; + set->n = o; +} + +/* Emit the parts of [l, r) not covered by @set; returns pieces written. */ +static int _span_set_subtract(const twin_span_set_t *set, + twin_coord_t l, + twin_coord_t r, + twin_span_t *out, + int max_out) +{ + int n = 0; + for (int i = 0; i < set->n && l < r; i++) { + const twin_span_t *s = &set->s[i]; + if (s->r <= l) + continue; + if (s->l >= r) + break; + if (s->l > l) { + if (n == max_out) + return -1; + out[n++] = (twin_span_t) {l, s->l}; + } + l = max(l, s->r); + } + if (l < r) { + if (n == max_out) + return -1; + out[n++] = (twin_span_t) {l, r}; + } + return n; +} + +#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. + */ +static uint16_t shadow_ramp[2 * TWIN_SHADOW_MAX_BLUR + 1]; +/* Widest shadow piece whose alphas are kept between rows */ +#define SHADOW_ROW_CACHE 64 +static int shadow_ramp_blur = -1; + +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; + } + return shadow_ramp; +} + +/* Coverage of a blurred box edge pair at position @v, 0..256 */ +static inline int _shadow_cover(const uint16_t *ramp, + int blur, + twin_coord_t v, + twin_coord_t lo, + twin_coord_t hi) +{ + int n = 2 * blur + 1; + int rise = v - (lo - blur); + int fall = (hi + blur - 1) - v; + int a = rise < 0 ? 0 : rise >= n ? 256 : ramp[rise]; + int b = fall < 0 ? 0 : fall >= n ? 256 : ramp[fall]; + int c = a + b - 256; + return c < 0 ? 0 : c; +} + +static inline twin_rect_t _shadow_rect(const twin_pixmap_t *p) +{ + 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; +} + +#endif /* CONFIG_DROP_SHADOW */ + +twin_rect_t _twin_pixmap_extents(const twin_pixmap_t *p) +{ + twin_rect_t r = {p->x, p->x + p->width, p->y, p->y + p->scan_height}; +#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); + } +#endif + return r; +} + +#if defined(CONFIG_DROP_SHADOW) +/* Premultiplied black at coverage @a (0..255) over dst: color scales, + * coverage adds. Two channels per multiply, rounding as twin_int_mult. */ +static inline twin_argb32_t _shadow_over(twin_argb32_t v, uint32_t a) +{ + uint32_t k = 255 - a; + uint32_t rb = _twin_mul_lanes(v & TWIN_LANES, k); + uint32_t ag = _twin_mul_lanes((v >> 8) & TWIN_LANES, k); + /* The scaled alpha is at most k, so adding a never carries out */ + 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) +{ + 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); - /* bounds check in x */ - p_left = left; - if (p_left < p->x) - p_left = p->x; - p_right = right; - if (p_right > p->x + p->width) - p_right = p->x + p->width; - if (p_left >= p_right) + /* 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 { + uint32_t row_alpha; + int blur; + twin_coord_t sl, sr, l, r; + 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; + } + for (twin_coord_t i = 0; i < r - l; i++) + if (row.a[i]) + d[i] = _shadow_over(d[i], row.a[i]); return; - dst.argb32 = span + (p_left - left); - src.p = twin_pixmap_pointer(p, p_left - p->x, y - p->y); - if (p->format == TWIN_RGB16) - op16(dst, src, p_right - p_left); - else - op32(dst, src, p_right - p_left); -} + } -void twin_screen_update(twin_screen_t *screen) + /* 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; + } + 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++; + } +} +#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) { - twin_coord_t left = screen->damage.left; - twin_coord_t top = screen->damage.top; - twin_coord_t right = screen->damage.right; - twin_coord_t bottom = screen->damage.bottom; - twin_src_op pop16, pop32, bop32; + (void) span, (void) span_left, (void) p, (void) y, (void) l, (void) r; +} +#endif - pop16 = _twin_rgb16_source_argb32; - pop32 = _twin_argb32_over_argb32; - bop32 = _twin_argb32_source_argb32; +/* + * One layer as seen by a single scanline. Shadows appear as their own + * entry directly below the pixmap casting them. + */ +#define TWIN_ROW_LAYERS 32 +#define TWIN_ROW_PIECES 64 - if (right > screen->width) - right = screen->width; - if (bottom > screen->height) - bottom = screen->height; +typedef struct { + twin_pixmap_t *p; + bool shadow; + bool opaque_full; /* every pixel on this row is opaque: use copy */ + twin_coord_t l, r; + int first, count; /* visible pieces in the piece pool */ +} twin_row_layer_t; - if (!screen->disable && left < right && top < bottom) { - twin_argb32_t *span; - twin_pixmap_t *p; - twin_coord_t y; - twin_coord_t width = right - left; +static inline bool _layer_hidden(const twin_pixmap_t *p) +{ + return !p->opacity || _twin_pixmap_is_stub(p); +} - screen->damage.left = screen->damage.right = 0; - screen->damage.top = screen->damage.bottom = 0; +static void _composite_layer(twin_argb32_t *span, + twin_coord_t span_left, + const twin_row_layer_t *layer, + twin_coord_t y, + twin_coord_t l, + twin_coord_t r) +{ + twin_pixmap_t *p = layer->p; + twin_pointer_t src = twin_pixmap_pointer(p, l - p->x, y - p->y); + twin_argb32_t *dst = span + (l - span_left); + int w = r - l; - /* Reuse cached span buffer if large enough */ - if (screen->span_cache && screen->span_cache_width >= width) { - span = screen->span_cache; + if (p->format == TWIN_RGB16) { + if (p->opacity == 0xff) { + twin_source_u s = {.p = src}; + _twin_rgb16_source_argb32((twin_pointer_t) {.argb32 = dst}, s, w); } else { - /* Need larger cache - reallocate */ - twin_argb32_t *new_cache = - twin_realloc(screen->span_cache, width * sizeof(twin_argb32_t)); - if (!new_cache) - return; - screen->span_cache = new_cache; - screen->span_cache_width = width; - span = new_cache; + _twin_screen_blend_rgb16(dst, src.rgb16, p->opacity, w); + } + } else if (p->format == TWIN_ARGB32) { + twin_source_u s = {.p = src}; + if (p->opacity != 0xff) + _twin_screen_blend_argb32(dst, src.argb32, p->opacity, w); + else if (layer->opaque_full) + memcpy(dst, src.argb32, (size_t) w * sizeof(*dst)); + else + _twin_argb32_over_argb32((twin_pointer_t) {.argb32 = dst}, s, w); + } else { + /* A8 pixmaps on screen draw as black coverage */ + for (int i = 0; i < w; i++) { + /* v / 255 for v <= 255 * 255, without a divide */ + uint32_t cov = (uint32_t) src.a8[i] * p->opacity; + uint32_t a = (cov + 1 + (cov >> 8)) >> 8; + uint32_t k = 255 - a; + uint16_t t0, t1, t2, t3; + twin_argb32_t v = dst[i]; + dst[i] = (twin_in(v, 0, k, t0) | twin_in(v, 8, k, t1) | + twin_in(v, 16, k, t2) | twin_in(v, 24, k, t3)) + + (a << 24); } + } +} - if (screen->put_begin) - (*screen->put_begin)(left, top, right, bottom, screen->closure); - for (y = top; y < bottom; y++) { - if (screen->background) { - twin_pointer_t dst; - twin_source_u src; - twin_coord_t p_left; - twin_coord_t m_left; - twin_coord_t p_this; - twin_coord_t p_width = screen->background->width; - twin_coord_t p_y = y % screen->background->height; - - for (p_left = left; p_left < right; p_left += p_this) { - dst.argb32 = span + (p_left - left); - m_left = p_left % p_width; - p_this = p_width - m_left; - if (p_left + p_this > right) - p_this = right - p_left; - src.p = - twin_pixmap_pointer(screen->background, m_left, p_y); - bop32(dst, src, p_this); +static void _background_span(twin_screen_t *screen, + twin_argb32_t *span, + twin_coord_t span_left, + twin_coord_t y, + twin_coord_t l, + twin_coord_t r) +{ + twin_pixmap_t *bg = screen->background; + if (!bg) { + _twin_screen_fill(span + (l - span_left), screen->background_color, + r - l); + return; + } + twin_coord_t p_width = bg->width; + twin_coord_t p_y = y % bg->height; + twin_coord_t p_this; + for (twin_coord_t x = l; x < r; x += p_this) { + twin_coord_t m_left = x % p_width; + p_this = p_width - m_left; + if (x + p_this > r) + p_this = r - x; + twin_pointer_t dst = {.argb32 = span + (x - span_left)}; + twin_source_u src = {.p = twin_pixmap_pointer(bg, m_left, p_y)}; + if (bg->format == TWIN_RGB16) + _twin_rgb16_source_argb32(dst, src, p_this); + else + _twin_argb32_source_argb32(dst, src, p_this); + } +} + +/* The columns where @p's own pixels are opaque on row @y, whatever its + * opacity */ +static bool _layer_solid_span(const twin_pixmap_t *p, + twin_coord_t y, + twin_coord_t *ol, + twin_coord_t * or) +{ + const twin_rect_t *o = &p->opaque_rect; + if (o->left >= o->right) + return false; + if (y < p->y + o->top || y >= p->y + o->bottom) + return false; + *ol = p->x + o->left; + *or = p->x + o->right; + return true; +} + +/* Is @p, on row @y, opaque on screen? Returns the opaque span. */ +static bool _layer_opaque_span(const twin_pixmap_t *p, + twin_coord_t y, + twin_coord_t *ol, + twin_coord_t * or) +{ + if (p->opacity != 0xff || (_twin_debug & TWIN_DEBUG_NO_OCCLUSION)) + return false; + return _layer_solid_span(p, y, ol, or); +} + +static void _composite_row(twin_screen_t *screen, + twin_argb32_t *span, + twin_coord_t y, + twin_coord_t left, + twin_coord_t right) +{ + twin_row_layer_t layers[TWIN_ROW_LAYERS]; + twin_span_t pieces[TWIN_ROW_PIECES]; + twin_span_set_t covered = {0}; + int n_layers = 0, n_pieces = 0; + bool overflow = (_twin_debug & TWIN_DEBUG_ROW_FALLBACK) != 0; + + /* Collect layers top-down: cursor, then pixmaps (each before its + * shadow). */ + twin_pixmap_t *top = screen->top; +#if defined(CONFIG_CURSOR) + twin_pixmap_t *cursor = screen->cursor; +#else + twin_pixmap_t *cursor = NULL; +#endif + for (twin_pixmap_t *p = overflow ? NULL + : cursor ? cursor + : top; + p; p = (p == cursor) ? top : p->down) { + if (_layer_hidden(p)) + continue; + for (int pass = 0; pass < 2; pass++) { + twin_coord_t l, r; + bool is_shadow = pass == 1; + if (!is_shadow) { + if (y < p->y || y >= p->y + p->scan_height) + continue; + l = p->x; + r = p->x + p->width; + } else { + if (!_twin_pixmap_shadowed(p)) + break; + twin_rect_t e = _twin_pixmap_extents(p); + if (y < e.top || y >= e.bottom) + break; + l = e.left; + r = e.right; + } + l = max(l, left); + r = min(r, right); + if (l >= r) + continue; + if (n_layers == TWIN_ROW_LAYERS) { + overflow = true; + break; + } + twin_row_layer_t *ly = &layers[n_layers++]; + ly->p = p; + ly->shadow = is_shadow; + ly->l = l; + ly->r = r; + ly->opaque_full = false; + + /* Visible part = layer extent minus everything opaque above */ + int room = TWIN_ROW_PIECES - n_pieces; + int got = + _span_set_subtract(&covered, l, r, pieces + n_pieces, room); + if (got < 0) { + if (room < 1) { + overflow = true; + break; } - } else - memset(span, 0xff, width * sizeof(twin_argb32_t)); - - for (p = screen->bottom; p; p = p->up) { - /* Skip drawing the region of the iconified pixmap. */ - if (!twin_pixmap_is_iconified(p, y)) - twin_screen_span_pixmap(screen, span, p, y, left, right, - pop16, pop32); + pieces[n_pieces] = (twin_span_t) {l, r}; /* conservative */ + got = 1; } + ly->first = n_pieces; + ly->count = got; + n_pieces += got; + twin_coord_t ol, or ; + if (!is_shadow && _layer_opaque_span(p, y, &ol, &or)) { + ly->opaque_full = (ol <= p->x && p->x + p->width <= or); + _span_set_add(&covered, max(ol, left), min(or, right)); + } + } + if (overflow) + break; + if (p == cursor && !top) + break; + } + + if (overflow) { + /* Too many layers on this row: composite everything bottom-up. */ + _background_span(screen, span, left, y, left, right); + for (twin_pixmap_t *p = screen->bottom; p; p = p->up) { + if (_layer_hidden(p)) + continue; + if (_twin_pixmap_shadowed(p)) { + twin_rect_t e = _twin_pixmap_extents(p); + if (y >= e.top && y < e.bottom) + _shadow_span(span, left, p, y, max(e.left, left), + min(e.right, right)); + } + if (y < p->y || y >= p->y + p->scan_height) + continue; + twin_row_layer_t ly = {.p = p}; + twin_coord_t l = max(p->x, left); + twin_coord_t r = min((twin_coord_t) (p->x + p->width), right); + if (l < r) + _composite_layer(span, left, &ly, y, l, r); + } #if defined(CONFIG_CURSOR) - if (screen->cursor) - twin_screen_span_pixmap(screen, span, screen->cursor, y, left, - right, pop16, pop32); + if (cursor && y >= cursor->y && y < cursor->y + cursor->height) { + twin_row_layer_t ly = {.p = cursor}; + twin_coord_t l = max(cursor->x, left); + twin_coord_t r = + min((twin_coord_t) (cursor->x + cursor->width), right); + if (l < r) + _composite_layer(span, left, &ly, y, l, r); + } #endif + screen->stats.layer_pixels += right - left; + return; + } + + /* A piece of a translucent layer whose own pixels are opaque, with no + * layer below it, lands on the solid background: blend it against the + * color directly and skip filling the background there. This is the + * window a phone transition dims, every frame. */ + uint64_t onto = 0; + if (!screen->background && !(_twin_debug & TWIN_DEBUG_NO_OCCLUSION)) { + for (int i = 0; i < n_layers; i++) { + twin_row_layer_t *ly = &layers[i]; + twin_coord_t ol, or ; + if (ly->shadow || ly->p->opacity == 0xff || + ly->p->format == TWIN_A8 || + !_layer_solid_span(ly->p, y, &ol, &or)) + continue; + for (int k = 0; k < ly->count; k++) { + twin_span_t *pc = &pieces[ly->first + k]; + bool below = false; + for (int j = i + 1; j < n_layers && !below; j++) + below = layers[j].l < pc->r && pc->l < layers[j].r; + if (below || pc->l < ol || pc->r > or) + continue; + onto |= (uint64_t) 1 << (ly->first + k); + _span_set_add(&covered, pc->l, pc->r); + } + } + } + + /* Background where nothing opaque lands */ + { + twin_span_t bg[TWIN_SPAN_SET_MAX + 1]; + int n = _span_set_subtract(&covered, left, right, bg, + TWIN_SPAN_SET_MAX + 1); + for (int i = 0; i < n; i++) + _background_span(screen, span, left, y, bg[i].l, bg[i].r); + } + + /* Bottom-up over the visible pieces */ + 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++) { + 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))) { + twin_pixmap_t *p = ly->p; + twin_pointer_t src = + twin_pixmap_pointer(p, pc->l - p->x, y - p->y); + twin_argb32_t *dst = span + (pc->l - left); + if (p->format == TWIN_RGB16) + _twin_screen_blend_onto_rgb16(dst, src.rgb16, p->opacity, + screen->background_color, + pc->r - pc->l); + else + _twin_screen_blend_onto_argb32(dst, src.argb32, p->opacity, + screen->background_color, + pc->r - pc->l); + } else { + _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; + } + } +} +void twin_screen_update(twin_screen_t *screen) +{ + if (screen->disable || !screen->n_damage) + return; + + /* Size the span buffer first: if that fails, the damage stays queued + * for the next update instead of being lost. */ + int n = screen->n_damage; + twin_coord_t max_width = 0; + for (int i = 0; i < n; i++) + max_width = max(max_width, (twin_coord_t) (screen->damage[i].right - + screen->damage[i].left)); + if (!screen->span_cache || screen->span_cache_width < max_width) { + twin_argb32_t *c = + twin_realloc(screen->span_cache, max_width * sizeof(twin_argb32_t)); + if (!c) + return; + screen->span_cache = c; + screen->span_cache_width = max_width; + } + twin_argb32_t *span = screen->span_cache; + + /* Snapshot and clear before scan-out: backends may damage meanwhile. */ + twin_rect_t rects[TWIN_DAMAGE_RECTS]; + memcpy(rects, screen->damage, sizeof(rects[0]) * n); + screen->n_damage = 0; + + screen->stats.frames++; + for (int i = 0; i < n; i++) { + twin_coord_t left = rects[i].left, right = rects[i].right; + twin_coord_t top = rects[i].top, bottom = rects[i].bottom; + right = min(right, screen->width); + bottom = min(bottom, screen->height); + if (left >= right || top >= bottom) + continue; + screen->stats.rects++; + if (screen->put_begin) + (*screen->put_begin)(left, top, right, bottom, screen->closure); + for (twin_coord_t y = top; y < bottom; y++) { + _composite_row(screen, span, y, left, right); (*screen->put_span)(left, y, right, span, screen->closure); } - /* Span buffer is now cached - don't free */ + screen->stats.pixels += (uint64_t) (right - left) * (bottom - top); } } void twin_screen_set_active(twin_screen_t *screen, twin_pixmap_t *pixmap) { - twin_event_t ev; + twin_event_t ev = {0}; twin_pixmap_t *old = screen->active; + if (old == pixmap) + return; screen->active = pixmap; + /* Handlers run under the busy guard */ + _twin_screen_busy(screen); if (old) { ev.kind = TwinEventDeactivate; twin_pixmap_dispatch(old, &ev); } - if (pixmap) { + /* The deactivated window may have destroyed or replaced the new one */ + if (pixmap && screen->active == pixmap) { ev.kind = TwinEventActivate; twin_pixmap_dispatch(pixmap, &ev); } -} - -twin_pixmap_t *twin_screen_get_active(twin_screen_t *screen) -{ - return screen->active; + _twin_screen_idle(screen); } void twin_screen_set_background(twin_screen_t *screen, twin_pixmap_t *pixmap) @@ -287,11 +834,6 @@ void twin_screen_set_background(twin_screen_t *screen, twin_pixmap_t *pixmap) twin_screen_damage(screen, 0, 0, screen->width, screen->height); } -twin_pixmap_t *twin_screen_get_background(twin_screen_t *screen) -{ - return screen->background; -} - #if defined(CONFIG_CURSOR) static void twin_screen_damage_cursor(twin_screen_t *screen) { @@ -302,13 +844,16 @@ static void twin_screen_damage_cursor(twin_screen_t *screen) void twin_screen_set_cursor(twin_screen_t *screen, twin_pixmap_t *pixmap, - twin_fixed_t hotspot_x, - twin_fixed_t hotspot_y) + twin_coord_t hotspot_x, + twin_coord_t hotspot_y) { twin_screen_disable_update(screen); - if (screen->cursor) + if (screen->cursor) { twin_screen_damage_cursor(screen); + if (screen->cursor != pixmap) + twin_pixmap_destroy(screen->cursor); + } screen->cursor = pixmap; screen->curs_hx = hotspot_x; @@ -351,10 +896,101 @@ static void _twin_adj_mouse_evt(twin_event_t *event, event->u.pointer.y = event->u.pointer.screen_y - pixmap->y; } +static twin_pixmap_t *_twin_screen_pick(twin_screen_t *screen, + twin_coord_t x, + twin_coord_t y) +{ + for (twin_pixmap_t *p = screen->top; p; p = p->down) { + if (p->window && _twin_window_hit(p->window, x, y)) + return p; + } + return NULL; +} + +void _twin_screen_busy(twin_screen_t *screen) +{ + screen->dispatching++; +} + +void _twin_screen_idle(twin_screen_t *screen) +{ + if (--screen->dispatching) + return; + /* Nobody can hold a destroyed window any more */ + while (screen->zombies) { + twin_window_t *w = screen->zombies; + screen->zombies = w->zombie_next; + twin_free(w); + } +} + +/* Send @event to @pixmap as @kind, in @pixmap's coordinates */ +static bool _twin_screen_send(twin_pixmap_t *pixmap, + const twin_event_t *event, + twin_event_kind_t kind) +{ + twin_event_t evt = *event; + evt.kind = kind; + _twin_adj_mouse_evt(&evt, pixmap); + return twin_pixmap_dispatch(pixmap, &evt); +} + +/* Point screen->target at the window under the pointer, sending Leave and + * Enter. Handlers may destroy windows: the old target is re-checked + * through screen->target (destroying a pixmap clears it) and the new one + * is picked again after Leave. */ +static void _twin_screen_hover(twin_screen_t *screen, const twin_event_t *event) +{ + twin_coord_t x = event->u.pointer.screen_x, y = event->u.pointer.screen_y; + twin_pixmap_t *ntarget = _twin_screen_pick(screen, x, y); + if (screen->target == ntarget) + return; + if (screen->target) { + _twin_screen_send(screen->target, event, TwinEventLeave); + ntarget = _twin_screen_pick(screen, x, y); + } + screen->target = ntarget; + if (ntarget) + _twin_screen_send(ntarget, event, TwinEventEnter); +} + +bool _twin_screen_route(twin_screen_t *screen, twin_event_t *event) +{ + /* A grab sends everything up to the release to the pressed window */ + if (screen->clicklock) { + twin_pixmap_t *grab = screen->target; + if (event->kind != TwinEventButtonUp) + return grab && _twin_screen_send(grab, event, event->kind); + /* The release ends the grab: deliver it where the press went, then + * let hover catch up with the pointer */ + screen->clicklock = 0; + bool handled = grab && _twin_screen_send(grab, event, event->kind); + _twin_screen_hover(screen, event); + return handled; + } + + _twin_screen_hover(screen, event); + twin_pixmap_t *pixmap = screen->target; + if (!pixmap) + return false; + if (event->kind == TwinEventButtonDown) + screen->clicklock = 1; + return _twin_screen_send(pixmap, event, event->kind); +} + +static bool _twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event); + bool twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event) { - twin_pixmap_t *pixmap, *ntarget; + /* Handlers may destroy windows; they are freed once we are out */ + _twin_screen_busy(screen); + bool handled = _twin_screen_dispatch(screen, event); + _twin_screen_idle(screen); + return handled; +} +static bool _twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event) +{ if (screen->event_filter && screen->event_filter(screen, event)) return true; @@ -363,71 +999,33 @@ bool twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event) case TwinEventButtonDown: case TwinEventButtonUp: #if defined(CONFIG_CURSOR) - /* update mouse cursor */ twin_screen_update_cursor(screen, event->u.pointer.screen_x, event->u.pointer.screen_y); #endif - - /* if target is tracking the mouse, check for mouse up, if not, - * just pass the event along - */ - pixmap = screen->target; - if (screen->clicklock && event->kind != TwinEventButtonUp) - goto mouse_passup; - - /* if event is mouse up, stop tracking if any */ - if (event->kind == TwinEventButtonUp) - screen->clicklock = 0; - - /* check who the mouse is over now */ - for (ntarget = screen->top; ntarget; ntarget = ntarget->down) - if (_twin_window_valid_range(ntarget->window, - event->u.pointer.screen_x, - event->u.pointer.screen_y)) - break; - - /* ah, somebody new ... send leave/enter events and set new target */ - if (pixmap != ntarget) { - twin_event_t evt; - - if (pixmap) { - evt = *event; - evt.kind = TwinEventLeave; - _twin_adj_mouse_evt(&evt, pixmap); - twin_pixmap_dispatch(pixmap, &evt); - } - - pixmap = screen->target = ntarget; - - if (pixmap) { - evt = *event; - _twin_adj_mouse_evt(&evt, pixmap); - evt.kind = TwinEventEnter; - twin_pixmap_dispatch(pixmap, &evt); - } - } - - /* check for new click locking */ - if (pixmap && event->kind == TwinEventButtonDown) - screen->clicklock = 1; - - mouse_passup: - /* adjust event to pixmap coordinates before passing up */ - if (pixmap) - _twin_adj_mouse_evt(event, pixmap); - break; - + /* Window manager gestures see pointer events first */ + if (_twin_wm_filter(screen, event)) + return true; + return _twin_screen_route(screen, event); case TwinEventKeyDown: case TwinEventKeyUp: case TwinEventUcs4: - pixmap = screen->active; - break; - + return screen->active && twin_pixmap_dispatch(screen->active, event); default: - pixmap = NULL; - break; + return false; } - if (pixmap) - return twin_pixmap_dispatch(pixmap, event); - return false; +} + +twin_coord_t twin_screen_width(const twin_screen_t *screen) +{ + return screen->width; +} + +twin_coord_t twin_screen_height(const twin_screen_t *screen) +{ + return screen->height; +} + +const twin_screen_stats_t *twin_screen_stats(const twin_screen_t *screen) +{ + return &screen->stats; } diff --git a/src/text.c b/src/text.c new file mode 100644 index 00000000..e1c6d65c --- /dev/null +++ b/src/text.c @@ -0,0 +1,213 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Glyph-cached text + * + * twin_path_utf8() turns a whole string into one outline, which is the right + * tool for transformed text but repeats the outline decode, curve + * flattening and supersampled scan conversion for every glyph on every + * repaint. UI text is axis aligned and drawn at a handful of sizes, so this + * path rasterizes each (font, size, code point) once into the coverage cache + * and afterwards only composites the cached mask. + * + * Glyph origins snap to whole pixels; advances accumulate in 16.16 so line + * length matches twin_text_width() exactly. + */ + +#include "twin_private.h" + +static void _twin_path_init_local(twin_path_t *path, + twin_font_t *font, + twin_fixed_t size) +{ + _twin_path_init(path); + path->state.font = font; + path->state.font_size = size; +} + +static inline twin_font_t *_font_or_default(twin_font_t *font) +{ + return font ? font : g_twin_font; +} + +void twin_font_extents(twin_font_t *font, + twin_fixed_t size, + twin_fixed_t *ascent, + twin_fixed_t *descent) +{ + font = _font_or_default(font); + twin_fixed_t a, d; + if (font->type == TWIN_FONT_TYPE_TTF) { + a = (twin_fixed_t) (((int64_t) font->ascender * size) >> 6); + d = (twin_fixed_t) (((int64_t) -font->descender * size) >> 6); + } else { + d = size / 3; + a = size - d; + } + if (ascent) + *ascent = a; + if (descent) + *descent = d; +} + +twin_fixed_t twin_text_width(twin_font_t *font, + twin_fixed_t size, + const char *utf8) +{ + twin_path_t path; + _twin_path_init_local(&path, _font_or_default(font), size); + twin_fixed_t w = twin_width_utf8(&path, utf8); + _twin_path_fini(&path); + return w; +} + +static uint64_t _glyph_key(const twin_font_t *font, + twin_fixed_t size, + twin_ucs4_t ucs4) +{ + uint64_t h = TWIN_HASH_INIT ^ 0x47; /* 'G': glyph domain */ + uintptr_t f = (uintptr_t) font; + h = _twin_hash(h, &f, sizeof(f)); + h = _twin_hash(h, &size, sizeof(size)); + h = _twin_hash(h, &ucs4, sizeof(ucs4)); + return h; +} + +/* Glyph outline at the origin, in device space; returns the advance */ +static twin_fixed_t _glyph_outline(twin_path_t *path, + twin_font_t *font, + twin_fixed_t size, + twin_ucs4_t ucs4) +{ + _twin_path_init_local(path, font, size); + twin_path_move(path, 0, 0); + /* Measured at the origin, as twin_text_width() measures */ + twin_fixed_t advance = twin_width_ucs4(path, ucs4); + twin_path_ucs4(path, ucs4); + /* Drop the trailing pen move so the bounds cover ink only */ + if (path->npoints) + path->npoints--; + if (path->nsublen && path->sublen[path->nsublen - 1] >= path->npoints) + path->nsublen--; + return advance; +} + +/* + * Ink bounds. Point 0 is the pen origin that twin_path_ucs4() starts from; + * its edges to and from the first contour cancel, so it carries no coverage + * and must not stretch the mask down to the baseline. + */ +static void _glyph_ink_bounds(const twin_path_t *path, twin_rect_t *rect) +{ + _twin_path_bounds_from(path, 1, rect); +} + +/* + * Look the glyph up, rasterizing on a miss. When the cache cannot hold it + * (disabled, or larger than an entry may be), the coverage is rendered into + * @tmp, which the caller frees. + */ +static const twin_cache_entry_t *_glyph_get(twin_font_t *font, + twin_fixed_t size, + twin_ucs4_t ucs4, + twin_cache_entry_t **tmp) +{ + uint64_t key = _glyph_key(font, size, ucs4); + twin_cache_entry_t *e = _twin_cache_lookup(key); + if (e) + return e; + + twin_path_t path; + twin_rect_t b; + twin_fixed_t advance = _glyph_outline(&path, font, size, ucs4); + _glyph_ink_bounds(&path, &b); + twin_coord_t w = b.right - b.left, h = b.bottom - b.top; + if (w <= 0 || h <= 0) { + w = h = 0; + b.left = b.top = 0; + } + + e = _twin_cache_insert(key, w, h); + if (!e) { + e = *tmp = _twin_cache_entry_alloc(w, h); + if (!e) { + _twin_path_fini(&path); + return NULL; + } + } + e->left = b.left; + e->top = b.top; + e->advance = advance; + if (w && h) { + twin_pixmap_t mask; + _twin_pixmap_init_a8(&mask, w, h, e->stride, e->data); + twin_fill_path(&mask, &path, -b.left, -b.top, NULL); + } + _twin_path_fini(&path); + return e; +} + +twin_fixed_t twin_text_draw(twin_pixmap_t *dst, + twin_font_t *font, + twin_fixed_t size, + twin_fixed_t x, + twin_fixed_t y, + twin_argb32_t color, + const char *utf8) +{ + font = _font_or_default(font); + + /* Whole line outside the clip vertically: measure only. Glyphs may + * overshoot the nominal extents (accents, brackets, stroke pens), so + * allow half an em of slack either way. */ + twin_fixed_t ascent, descent; + twin_font_extents(font, size, &ascent, &descent); + twin_coord_t slack = twin_fixed_to_int(size / 2) + 2; + twin_coord_t baseline = twin_fixed_to_int(y + TWIN_FIXED_HALF); + twin_coord_t line_top = baseline - twin_fixed_to_int(ascent) - slack; + twin_coord_t line_bottom = baseline + twin_fixed_to_int(descent) + slack; + twin_rect_t clip = twin_pixmap_get_clip(dst); + if (line_bottom <= clip.top || line_top >= clip.bottom) + return twin_text_width(font, size, utf8); + + twin_operand_t src = {.source_kind = TWIN_SOLID, .u.argb = color}; + twin_fixed_t pen = x; + twin_ucs4_t ucs4; + int len; + twin_pixmap_t mask; /* header reused for every glyph */ + _twin_pixmap_init_a8(&mask, 0, 0, 0, NULL); + twin_operand_t msk = {.source_kind = TWIN_PIXMAP, .u.pixmap = &mask}; + + while ((len = _twin_utf8_to_ucs4(utf8, &ucs4)) > 0) { + twin_coord_t gx = twin_fixed_to_int(pen + TWIN_FIXED_HALF); + + /* Past the right edge: the rest is only measured below. Ink can + * start left of the pen (negative bearing), hence the slack. */ + if (gx - slack > clip.right) + break; + + twin_cache_entry_t *tmp = NULL; + const twin_cache_entry_t *e = _glyph_get(font, size, ucs4, &tmp); + if (!e) + break; /* out of memory: measure this glyph and the rest */ + utf8 += len; + if (e->width && e->height && gx + e->left + e->width > clip.left) { + mask.width = mask.clip.right = e->width; + mask.height = mask.clip.bottom = e->height; + mask.stride = e->stride; + mask.p.v = (void *) e->data; + twin_composite(dst, gx + e->left, baseline + e->top, &src, 0, 0, + &msk, 0, 0, TWIN_OVER, e->width, e->height); + } + pen += e->advance; + twin_free(tmp); + } + /* Anything not drawn (past the right clip) still counts */ + if (*utf8) + pen += twin_text_width(font, size, utf8); + return pen - x; +} diff --git a/src/timeout.c b/src/timeout.c index 6c9b2873..fa27c64a 100644 --- a/src/timeout.c +++ b/src/timeout.c @@ -10,13 +10,35 @@ #include "twin_private.h" -#define twin_time_compare(a, op, b) (((a) - (b)) op 0) +static twin_time_t (*_twin_time_source)(void); -static twin_time_t twin_now(void) +/* Millisecond time wraps; do the arithmetic unsigned to avoid overflow */ +static inline twin_time_t _twin_time_add(twin_time_t t, twin_time_t d) { + return (twin_time_t) ((uint32_t) t + (uint32_t) d); +} + +void _twin_set_time_source(twin_time_t (*now)(void)) +{ + _twin_time_source = now; +} + +twin_time_t twin_now(void) +{ + if (_twin_time_source) + return _twin_time_source(); + /* Wraps every ~24 days; callers compare with twin_time_compare(). */ +#ifdef CLOCK_MONOTONIC + /* Immune to the wall clock being set (RTC or NTP after boot) */ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) + return (twin_time_t) ((uint32_t) ts.tv_sec * 1000u + + (uint32_t) ts.tv_nsec / 1000000u); +#endif struct timeval tv; gettimeofday(&tv, NULL); - return tv.tv_sec * 1000 + tv.tv_usec / 1000; + return (twin_time_t) ((uint32_t) tv.tv_sec * 1000u + + (uint32_t) tv.tv_usec / 1000u); } static twin_queue_t *head; @@ -92,15 +114,20 @@ void _twin_run_timeout(void) for (timeout = first; timeout && twin_time_compare(now, >=, timeout->time); timeout = (twin_timeout_t *) timeout->queue.order) { - /* Validate closure pointer using closure lifetime tracking */ - if (!_twin_closure_is_valid(timeout->closure)) + /* Cleared earlier in this pass (its closure is gone too) */ + if (timeout->queue.deleted) + continue; + /* Owner gone: drop the timeout instead of skipping it forever */ + if (timeout->closure && !_twin_closure_is_valid(timeout->closure)) { + _twin_delete_timeout(timeout); continue; + } delay = (*timeout->proc)(now, timeout->closure); if (timeout->queue.deleted) { continue; } else if (delay >= 0) { - timeout->time = twin_now() + delay; + timeout->time = _twin_time_add(twin_now(), delay); _twin_queue_reorder(&head, _twin_timeout_order, &timeout->queue); } else { @@ -129,8 +156,11 @@ void _twin_run_timeout(void) timeout = snapshot[i]; if (timeout->queue.deleted) continue; - if (!_twin_closure_is_valid(timeout->closure)) + if (timeout->closure && + !_twin_closure_is_valid(timeout->closure)) { + _twin_delete_timeout(timeout); continue; + } if (!twin_time_compare(now, >=, timeout->time)) break; @@ -138,7 +168,7 @@ void _twin_run_timeout(void) if (timeout->queue.deleted) { continue; } else if (delay >= 0) { - timeout->time = twin_now() + delay; + timeout->time = _twin_time_add(twin_now(), delay); _twin_queue_reorder(&head, _twin_timeout_order, &timeout->queue); } else { @@ -169,13 +199,11 @@ twin_timeout_t *twin_set_timeout(twin_timeout_proc_t timeout_proc, /* Register closure with lifetime tracking if provided */ if (closure) { + /* One reference per pending timeout, dropped on release */ if (!_twin_closure_register(closure)) { - /* Failed to register closure */ twin_free(timeout); return NULL; } - /* Add reference for this timeout */ - _twin_closure_ref(closure); } if (!start) @@ -183,7 +211,7 @@ twin_timeout_t *twin_set_timeout(twin_timeout_proc_t timeout_proc, timeout->delay = delay; timeout->proc = timeout_proc; timeout->closure = closure; - _twin_queue_timeout(timeout, twin_now() + delay); + _twin_queue_timeout(timeout, _twin_time_add(twin_now(), delay)); return timeout; } @@ -195,6 +223,16 @@ void twin_clear_timeout(twin_timeout_t *timeout) _twin_delete_timeout(timeout); } +twin_time_t _twin_idle_timeout(twin_screen_t *screen, twin_time_t cap) +{ + if (screen && twin_screen_damaged(screen)) + return 0; + twin_time_t wait = _twin_timeout_delay(); + if (cap >= 0 && (wait < 0 || wait > cap)) + wait = cap; + return wait; +} + twin_time_t _twin_timeout_delay(void) { if (head) { @@ -202,7 +240,7 @@ twin_time_t _twin_timeout_delay(void) twin_time_t now = twin_now(); if (twin_time_compare(now, >=, thead->time)) return 0; - return thead->time - now; + return (twin_time_t) ((uint32_t) thead->time - (uint32_t) now); } return -1; } diff --git a/src/toplevel.c b/src/toplevel.c index 20bf1301..647f5c8b 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -43,6 +43,10 @@ static void _twin_toplevel_draw(twin_window_t *window) twin_toplevel_t *toplevel = window->client_data; twin_event_t event; + /* The window asks for its damaged pixels, which may be all of them (a + * window manager restoring an evicted window): repaint every widget, + * not only those queued since the last paint */ + _twin_box_mark_paint(&toplevel->box); twin_screen_disable_update(window->screen); event.kind = TwinEventPaint; (*(&toplevel->box.widget)->handler)(&toplevel->box.widget, &event, NULL); @@ -60,8 +64,22 @@ static void _twin_toplevel_destroy(twin_window_t *window) /* Unregister from closure tracking */ _twin_closure_unregister(toplevel); + /* Refuse new paint/layout work while the widgets are torn down: the + * Destroy event reaches application callbacks, which may touch widgets + * and would otherwise queue work on a toplevel about to be freed. */ + toplevel->box.widget.paint = true; + toplevel->box.widget.layout = true; + event.kind = TwinEventDestroy; (*(&toplevel->box.widget)->handler)(&toplevel->box.widget, &event, NULL); + + /* Queued work must not outlive the toplevel: its address may be reused + * by an object the tracker would then mistake for it */ + twin_clear_work(toplevel->paint_work); + twin_clear_work(toplevel->layout_work); + toplevel->paint_work = toplevel->layout_work = NULL; + window->client_data = NULL; + twin_free(toplevel); } void _twin_toplevel_init(twin_toplevel_t *toplevel, @@ -81,8 +99,6 @@ void _twin_toplevel_init(twin_toplevel_t *toplevel, } twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, - twin_format_t format, - twin_window_style_t style, twin_coord_t x, twin_coord_t y, twin_coord_t width, @@ -90,25 +106,29 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, const char *name) { twin_toplevel_t *toplevel; - twin_window_t *window = - twin_window_create(screen, format, style, x, y, width, height); + twin_window_t *window = twin_window_create( + screen, TWIN_ARGB32, TwinWindowApplication, x, y, width, height); if (!window) return NULL; - toplevel = twin_malloc(sizeof(twin_toplevel_t) + strlen(name) + 1); + toplevel = twin_calloc(1, sizeof(twin_toplevel_t) + strlen(name) + 1); if (!toplevel) { twin_window_destroy(window); return NULL; } _twin_toplevel_init(toplevel, _twin_toplevel_dispatch, window, name); + /* An opaque root: every client pixel is painted (see the box's paint), + * so nothing behind shows through */ + twin_widget_set(&toplevel->box.widget, 0xffffffff); return toplevel; } static bool _twin_toplevel_paint(void *closure) { twin_toplevel_t *toplevel = closure; - twin_event_t ev; + twin_event_t ev = {0}; + toplevel->paint_work = NULL; twin_screen_disable_update(toplevel->box.widget.window->screen); ev.kind = TwinEventPaint; (*(&toplevel->box.widget)->handler)(&toplevel->box.widget, &ev, NULL); @@ -122,16 +142,19 @@ void _twin_toplevel_queue_paint(twin_widget_t *widget) if (!toplevel->box.widget.paint) { toplevel->box.widget.paint = true; - twin_set_work(_twin_toplevel_paint, TWIN_WORK_PAINT, toplevel); + toplevel->paint_work = + twin_set_work(_twin_toplevel_paint, TWIN_WORK_PAINT, toplevel); } } static bool _twin_toplevel_layout(void *closure) { twin_toplevel_t *toplevel = closure; - twin_event_t ev; + twin_event_t ev = {0}; twin_window_t *window = toplevel->box.widget.window; + toplevel->layout_work = NULL; + ev.kind = TwinEventQueryGeometry; (*(&toplevel->box.widget)->handler)(&toplevel->box.widget, &ev, NULL); ev.kind = TwinEventConfigure; @@ -149,14 +172,32 @@ void _twin_toplevel_queue_layout(twin_widget_t *widget) if (!toplevel->box.widget.layout) { toplevel->box.widget.layout = true; - twin_set_work(_twin_toplevel_layout, TWIN_WORK_LAYOUT, toplevel); + toplevel->layout_work = + twin_set_work(_twin_toplevel_layout, TWIN_WORK_LAYOUT, toplevel); _twin_toplevel_queue_paint(widget); } } void twin_toplevel_show(twin_toplevel_t *toplevel) { + twin_window_t *window = toplevel->box.widget.window; _twin_toplevel_layout(toplevel); - _twin_toplevel_paint(toplevel); - twin_window_show(toplevel->box.widget.window); + /* Paint every widget through the window, which also settles its damage: + * the window manager's first draw then has nothing left to repaint */ + twin_rect_t c = twin_window_client(window); + twin_window_damage(window, c.left, c.top, c.right, c.bottom); + twin_window_draw(window); + twin_window_show(window); +} + +void twin_toplevel_destroy(twin_toplevel_t *toplevel) +{ + /* The window's destroy callback tears down the widgets and frees the + * toplevel */ + twin_window_destroy(toplevel->box.widget.window); +} + +twin_box_t *twin_toplevel_box(twin_toplevel_t *toplevel) +{ + return &toplevel->box; } diff --git a/src/trig.c b/src/trig.c index 58a5a4b2..d952c837 100644 --- a/src/trig.c +++ b/src/trig.c @@ -7,38 +7,6 @@ #include "twin_private.h" -/* angles are measured from -2048 .. 2048 */ - -twin_fixed_t twin_sin(twin_angle_t a) -{ - twin_fixed_t sin_val = 0; - twin_sincos(a, &sin_val, NULL); - return sin_val; -} - -twin_fixed_t twin_cos(twin_angle_t a) -{ - twin_fixed_t cos_val = 0; - twin_sincos(a, NULL, &cos_val); - return cos_val; -} - -twin_fixed_t twin_tan(twin_angle_t a) -{ - twin_fixed_t s, c; - twin_sincos(a, &s, &c); - - if (c == 0) { - if (s > 0) - return TWIN_FIXED_MAX; - return TWIN_FIXED_MIN; - } - if (s == 0) - return 0; - - return ((s << 15) / c) << 1; -} - static inline twin_fixed_t sin_poly(twin_angle_t x) { /* S(x) = x * 2^(-n) * (A1 - 2 ^ (q-p) * x * (2^-n) * x * 2^(-n) * (B1 - 2 ^ @@ -159,34 +127,6 @@ static twin_angle_t twin_atan2_first_quadrant(twin_fixed_t y, twin_fixed_t x) return (twin_angle_t) (angle / 8); } -twin_angle_t twin_atan2(twin_fixed_t y, twin_fixed_t x) -{ - if (x == 0 && y == 0) - return TWIN_ANGLE_0; - if (x == 0) - return (y > 0) ? TWIN_ANGLE_90 : TWIN_ANGLE_270; - if (y == 0) - return (x > 0) ? TWIN_ANGLE_0 : TWIN_ANGLE_180; - - twin_fixed_t x_sign_mask = x >> 31; - twin_fixed_t abs_x = (x ^ x_sign_mask) - x_sign_mask; - twin_fixed_t y_sign_mask = y >> 31; - twin_fixed_t abs_y = (y ^ y_sign_mask) - y_sign_mask; - twin_fixed_t m = ((~x_sign_mask & ~y_sign_mask) * 0) + - ((x_sign_mask & ~y_sign_mask) * 1) + - ((x_sign_mask & y_sign_mask) * 1) + - ((~x_sign_mask & y_sign_mask) * 2); - twin_fixed_t sign = 1 - 2 * (x_sign_mask ^ y_sign_mask); - twin_angle_t angle = twin_atan2_first_quadrant(abs_y, abs_x); - - /* First quadrant : angle - * Second quadrant : 180 - angle - * Third quadrant : 180 + angle - * Fourth quadrant : 360 - angle - */ - return TWIN_ANGLE_180 * m + sign * angle; -} - twin_angle_t twin_acos(twin_fixed_t x) { if (x <= -TWIN_FIXED_ONE) diff --git a/src/twin_private.h b/src/twin_private.h index b3cc6b7e..463b06be 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -16,6 +16,454 @@ #include "twin.h" +/* Types the library uses internally */ +typedef uint16_t twin_a16_t; /* 16-bit alpha channel value */ +typedef uint16_t twin_rgb16_t; /* 16-bit RGB color (5-6-5 format) */ +typedef int32_t twin_area_t; /* Area measurement in pixels */ +typedef bool (*twin_work_proc_t)(void *closure); +typedef struct _twin_work twin_work_t; + +#define twin_bytes_per_pixel(format) (1 << (twin_coord_t) (format)) + +#define twin_fixed_ceil(f) (((f) + 0xffff) & ~0xffff) +#define twin_fixed_floor(f) ((f) & ~0xffff) + +/* Wrap-safe: the difference is taken modulo 2^32 */ +#define twin_time_compare(a, op, b) \ + ((twin_time_t) ((uint32_t) (a) - (uint32_t) (b)) op 0) + +#define _twin_widget_width(w) \ + (((twin_widget_t *) (w))->extents.right - \ + ((twin_widget_t *) (w))->extents.left) + +#define _twin_widget_height(w) \ + (((twin_widget_t *) (w))->extents.bottom - \ + ((twin_widget_t *) (w))->extents.top) + +/* + * Object layouts. The public header declares these types opaque; + * only the library and its backends see their fields. + */ + +/** + * Type-safe pointer union for different pixel formats + * + * Provides type-safe access to pixel data in different formats + * while maintaining memory efficiency through union storage. + */ +typedef union _twin_pointer { + void *v; /**< Generic void pointer */ + uint8_t *b; /**< Raw byte array pointer */ + twin_a8_t *a8; /**< 8-bit alpha format pointer */ + twin_rgb16_t *rgb16; /**< 16-bit RGB format pointer */ + twin_argb32_t *argb32; /**< 32-bit ARGB format pointer */ +} twin_pointer_t; + +/** + * Animation iterator for frame-by-frame playback + * + * 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. + */ +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 */ +}; + +/** + * 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. + */ +typedef struct _twin_shadow { + int8_t dx, dy; /**< Offset of the shadow rectangle */ + uint8_t blur; /**< Blur radius in pixels (0 disables the shadow) */ + twin_a8_t alpha; /**< Peak shadow opacity */ +} twin_shadow_t; + +/* The pixmap casts a shadow (only with CONFIG_DROP_SHADOW) */ +#if defined(CONFIG_DROP_SHADOW) +#define _twin_pixmap_shadowed(p) ((p)->shadow.blur && (p)->shadow.alpha) +#else +#define _twin_pixmap_shadowed(p) ((void) (p), false) +#endif + +/** + * Pixmap structure representing a rectangular array of pixels + * + * Core structure representing drawable surfaces with pixels. + * Can be displayed on screen or used as off-screen render targets. + */ +struct _twin_pixmap { + /* Screen management */ + struct _twin_screen *screen; /**< Screen displaying this pixmap */ + twin_count_t disable; /**< Update disable counter */ + + /* Display list linkage */ + struct _twin_pixmap *down, *up; /**< Z-order linked list pointers */ + + /* Position and layout */ + twin_coord_t x, y; /**< Screen position */ + twin_format_t format; /**< Pixel format */ + twin_coord_t width, height; /**< Dimensions in pixels */ + twin_coord_t stride; /**< Row stride in bytes */ + twin_matrix_t transform; /**< 2D transformation matrix */ + + /* Clipping and origin */ + twin_rect_t clip; /**< Clipping rectangle */ + twin_coord_t origin_x, origin_y; /**< Drawing origin offset */ + + /* Pixel data */ + twin_animation_t *animation; /**< Animation data if animated */ + twin_pointer_t p; /**< Pixel data pointer (NULL if evicted) */ + + /* Compositing attributes, applied by the screen compositor */ + twin_a8_t opacity; /**< Constant alpha applied on scan-out */ + twin_rect_t opaque_rect; /**< Region (pixmap coords) known to be opaque */ +#if defined(CONFIG_DROP_SHADOW) + twin_shadow_t shadow; /**< Procedural drop shadow */ +#endif + twin_coord_t scan_height; /**< Rows shown from the top on scan-out and + hit by the pointer (normally height) */ + + twin_window_t *window; /**< Associated window (if any) */ + + /* Transform buffer cache for compositing operations */ + void *xform_cache; /**< Cached xform buffer */ + size_t xform_cache_size; /**< Cached xform buffer size in bytes */ +}; + +/** + * Scanline drawing callback called before drawing operation begins + * @left : Left edge of drawing region + * @top : Top edge of drawing region + * @right : Right edge of drawing region + * @bottom : Bottom edge of drawing region + * @closure : User data pointer + * + * Backend callback invoked before any scanlines are drawn. + * Used for setup operations like buffer preparation. + */ +typedef void (*twin_put_begin_t)(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_coord_t bottom, + void *closure); + +/** + * Scanline drawing callback for outputting pixel data + * @left : Left X coordinate of scanline + * @top : Y coordinate of scanline + * @right : Right X coordinate of scanline + * @pixels : Array of ARGB32 pixel data to draw + * @closure : User data pointer + * + * Backend callback invoked for each scanline to be drawn. + * Pixels array contains (right-left+1) ARGB32 values. + */ +typedef void (*twin_put_span_t)(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure); + +struct _twin_wm_ops; + +/** Maximum number of disjoint damage rectangles tracked per frame */ +#define TWIN_DAMAGE_RECTS 8 + +/** + * Screen structure managing display and window system + * + * Manages the display surface and coordinates drawing operations. + * Maintains Z-order of pixmaps and handles input event routing. + */ +struct _twin_screen { + /* Display list management */ + twin_pixmap_t *top, *bottom; /**< Z-order display list */ + twin_pixmap_t *active; /**< Active pixmap for key events */ + + /* Mouse event targeting */ + twin_pixmap_t *target; /**< Current mouse event target */ + bool clicklock; /**< Mouse click lock state */ + + /* Cursor management */ + twin_pixmap_t *cursor; /**< Mouse cursor pixmap */ + twin_coord_t curs_hx, curs_hy; /**< Cursor hotspot offset */ + twin_coord_t curs_x, curs_y; /**< Current cursor position */ + + /* Display properties */ + twin_coord_t width, height; /**< Screen dimensions */ + twin_pixmap_t *background; /**< Background pattern */ + twin_argb32_t background_color; /**< Fill when no background pixmap */ + + /* Damage tracking: a short list of rectangles, merged when cheap */ + twin_rect_t damage[TWIN_DAMAGE_RECTS]; /**< Pending damage */ + int n_damage; /**< Valid entries in damage[] */ + twin_count_t disable; /**< Update disable counter */ + + /* Backend interface */ + twin_put_begin_t put_begin; /**< Scanline begin callback */ + twin_put_span_t put_span; /**< Scanline drawing callback */ + void *closure; /**< Backend user data */ + + /* Window manager policy */ + bool closing; /**< Being destroyed: no policy */ + twin_window_t *windows; /**< Every window, bottom to top */ + twin_window_t *zombies; /**< Destroyed while busy, freed when idle */ + int dispatching; /**< Busy depth: dispatch, relayout */ +#if defined(CONFIG_WM_MOBILE) + twin_window_t *bar_top; /**< System bar above the work area */ + twin_window_t *bar_bottom; /**< System bar below the work area */ + twin_coord_t inset_bottom; /**< Reserved above the bottom bar (IME) */ +#endif + + twin_screen_stats_t stats; /**< Compositor work counters */ + + /* Span buffer cache for screen updates */ + twin_argb32_t *span_cache; /**< Cached span buffer */ + twin_coord_t span_cache_width; /**< Cached span buffer width */ + + /* Scratch arena for per-frame temporary allocations */ + void *scratch_buf; /**< Scratch buffer (bump allocator) */ + size_t scratch_size; /**< Scratch buffer capacity in bytes */ + + /* Reusable path object cache to avoid repeated alloc/free */ +#define TWIN_PATH_CACHE_SIZE 4 + struct _twin_path *path_cache[TWIN_PATH_CACHE_SIZE]; + int path_cache_count; + + + /* Event processing: event filter callback */ + bool (*event_filter)(twin_screen_t *screen, twin_event_t *event); +}; + +#define UCS_PAGE_SHIFT 7 + +#define UCS_PER_PAGE (1 << UCS_PAGE_SHIFT) + +typedef struct _twin_charmap { + uint16_t page; /**< Unicode page number */ + uint16_t offsets[UCS_PER_PAGE]; /**< Glyph offsets in outlines, 0: none */ +} twin_charmap_t; + +#define TWIN_FONT_TYPE_STROKE 1 + +#define TWIN_FONT_TYPE_TTF 2 + +struct _twin_font { + /* Font definition - must be initialized */ + int type; /**< Font type (stroke or TTF) */ + const char *name; /**< Font name */ + const char *style; /**< Font style name */ + const twin_charmap_t *charmap; /**< Character map array */ + int n_charmap; /**< Number of character maps */ + const signed char *outlines; /**< Glyph outline data */ + signed char ascender; /**< Font ascender */ + signed char descender; /**< Font descender */ + signed char height; /**< Font height */ + + /* Runtime caching */ + const twin_charmap_t *cur_page; /**< Currently cached page */ +}; + +/* Font used by paths that did not select one with twin_path_set_font() */ +extern twin_font_t *g_twin_font; + +/* Built-in default stroke font */ +extern twin_font_t twin_Default_Font_Roman; + +struct _twin_window { + /* Window basics */ + twin_screen_t *screen; /**< Parent screen */ + twin_pixmap_t *pixmap; /**< Window pixmap */ + + /* Window properties */ + uint8_t style; /**< twin_window_style_t */ + uint8_t format; /**< twin_format_t of the window pixmap */ + twin_rect_t request; /**< Requested client geometry (screen) */ + twin_rect_t client; /**< Client area rectangle (pixmap) */ +#define TWIN_WINDOW_DAMAGE_RECTS 4 + twin_rect_t damage[TWIN_WINDOW_DAMAGE_RECTS]; /**< Pending repaint */ + int n_damage; /**< Valid damage[] */ + + /* Window state */ + bool active : 1; /**< Active window flag */ + bool client_grab : 1; /**< Mouse grab state */ + bool draw_queued : 1; /**< Draw pending flag */ + bool opaque : 1; /**< Client area is fully painted by draw() */ + bool mapped : 1; /**< Shown by the application */ + bool frame_dirty : 1; /**< Decorations need repainting */ + bool dead : 1; /**< Destroyed; the struct outlives a busy screen */ +#if defined(CONFIG_WM_DESKTOP) + bool wm_placed : 1; /**< Policy already chose a position */ + bool wm_maximized : 1; /**< Covers the work area; wm_saved is the request */ + twin_rect_t wm_saved; /**< Request to restore after maximize */ + char *name; /**< Title shown in the title bar */ +#endif + + struct _twin_work *paint_work; /**< Queued repaint, if any */ + + /* Window manager bookkeeping */ + twin_window_t *wm_next; /**< Next window in the screen's window list */ + twin_window_t *zombie_next; /**< Next in screen->zombies once dead */ + + /* Window data */ + void *client_data; /**< User data pointer */ + + /* Window callbacks */ + twin_draw_func_t draw; /**< Draw callback */ + twin_event_func_t event; /**< Event handler */ + twin_destroy_func_t destroy; /**< Destroy callback */ +}; + +typedef struct _twin_widget_layout { + twin_coord_t width, height; /**< Preferred dimensions */ + twin_stretch_t stretch_width; /**< Horizontal stretch factor */ + twin_stretch_t stretch_height; /**< Vertical stretch factor */ +} twin_widget_layout_t; + +/** + * Optional widget attributes, lazily allocated on first use. + * Widgets that never register a callback pay zero RAM cost for these + * fields. Allocated on first callback use. + */ +typedef struct _twin_widget_ext { + twin_widget_proc_t callback; /**< Application callback (optional) */ + void *callback_data; /**< Callback user data */ +} twin_widget_ext_t; + +struct _twin_widget { + /* Widget hierarchy */ + twin_window_t *window; /**< Parent window */ + twin_widget_t *next; /**< Next sibling widget */ + twin_box_t *parent; /**< Parent container */ + + /* Event handling */ + twin_widget_proc_t handler; /**< Widget event handler (framework) */ + twin_widget_ext_t *ext; /**< Optional attributes (lazy, may be NULL) */ + twin_rect_t extents; /**< Current geometry */ + twin_widget_t *copy_geom; /**< Geometry source widget (optional) */ + + /* Widget state */ + bool paint; /**< Needs painting */ + bool layout; /**< Needs layout */ + + /* Widget appearance */ + twin_argb32_t background; /**< Background color */ + twin_widget_layout_t preferred; /**< Preferred dimensions */ + twin_shape_t shape; /**< Widget shape */ +}; + +struct _twin_box { + twin_widget_t widget; /**< Base widget */ + twin_box_dir_t dir; /**< Layout direction */ + twin_widget_t *children; /**< Child widget list */ + twin_widget_t *button_down; /**< Button press target */ +}; + +struct _twin_toplevel { + twin_box_t box; /**< Base box container */ + struct _twin_work *paint_work; /**< Queued repaint, if any */ + struct _twin_work *layout_work; /**< Queued layout, if any */ +}; + +struct _twin_label { + twin_widget_t widget; /**< Base widget */ + char *label; /**< Label text */ + twin_argb32_t foreground; /**< Text color */ + twin_fixed_t font_size; /**< Font size */ + twin_style_t font_style; /**< Font style */ + twin_point_t offset; /**< Text offset */ + twin_align_t align; /**< Text alignment */ +}; + +struct _twin_button { + twin_label_t label; /**< Base label widget */ + bool pressed; /**< Button pressed state */ + bool active; /**< Button active state */ +}; + +struct _twin_context { + twin_screen_t *screen; /**< Screen context */ + void *priv; /**< Private backend data */ +}; + +/** + * Structure representing a custom widget with user data. + * Encapsulates a base widget and allows attaching custom data. + */ +struct _twin_custom_widget { + twin_widget_t widget; /**< First: the box lays it out and frees it */ + twin_custom_proc_t handler; /**< Application event handler */ + void *data; /**< Application data, in the same block */ +}; + +/* + * Backend interface: a backend creates the screen, feeds it events and + * scans it out. Applications only see the twin_create()/twin_run() layer. + */ + +/** + * Create screen for window system display + * @width : Screen width in pixels + * @height : Screen height in pixels + * @put_begin : Backend callback for drawing preparation + * @put_span : Backend callback for scanline output + * @closure : User data passed to backend callbacks + * + * Return Newly created screen, or NULL on failure + */ +twin_screen_t *twin_screen_create(twin_coord_t width, + twin_coord_t height, + twin_put_begin_t put_begin, + twin_put_span_t put_span, + void *closure); + +/** + * Destroy screen and free resources + * @screen : Screen to destroy + * + * Cleans up screen resources including all associated pixmaps. + */ +void twin_screen_destroy(twin_screen_t *screen); + +void twin_screen_damage(twin_screen_t *screen, + twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_coord_t bottom); + +void twin_screen_resize(twin_screen_t *screen, + twin_coord_t width, + twin_coord_t height); + +bool twin_screen_damaged(twin_screen_t *screen); + +void twin_screen_update(twin_screen_t *screen); + +bool twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event); + /* Portability: _Alignof is C11; older toolchains may need __alignof__ */ #if !defined(_Alignof) && !defined(__cplusplus) #if defined(__GNUC__) || defined(__clang__) @@ -46,11 +494,6 @@ static inline void twin_scratch_init(twin_scratch_t *s, void *buf, size_t sz) s->high_water = 0; } -static inline void twin_scratch_reset(twin_scratch_t *s) -{ - s->used = 0; -} - static inline size_t twin_scratch_save(twin_scratch_t *s) { return s->used; @@ -157,51 +600,20 @@ typedef int32_t twin_dfixed_t; typedef int8_t twin_gfixed_t; typedef int64_t twin_xfixed_t; -#define twin_sfixed_floor(f) ((f) & ~0xf) #define twin_sfixed_trunc(f) ((f) >> 4) #define twin_sfixed_ceil(f) (((f) + 0xf) & ~0xf) -#define twin_sfixed_mod(f) ((f) & 0xf) #define twin_int_to_sfixed(i) ((twin_sfixed_t) ((i) * 16)) -#define twin_sfixed_to_fixed(s) (((twin_fixed_t) (s)) << 12) +#define twin_sfixed_to_fixed(s) (((twin_fixed_t) (s)) * 4096) #define twin_fixed_to_sfixed(f) ((twin_sfixed_t) ((f) >> 12)) -#define twin_sfixed_to_dfixed(s) (((twin_dfixed_t) (s)) << 4) -#define twin_dfixed_to_sfixed(d) ((twin_sfixed_t) ((d) >> 4)) - #define twin_xfixed_to_fixed(x) ((twin_fixed_t) ((x) >> 16)) -#define twin_fixed_to_xfixed(f) (((twin_xfixed_t) (f)) << 16) - -/* - * twin_sfixed_t a = b'10100; - * twin_sfixed_t b = b'10000; - * exact a is 1*(2^0) + 1*(2^(-2)) = 1.25 - * exact b is 1*(2^0) = 1 - * - * The result of twin_sfixed_div(a, b) is of type twin_sfixed_t - * (a << 4) / (b) = (b'101000000) / (b'10000) = b'10100 - * exact result is 1*(2^0) + 1*(2^(-2)) = 1.25 - * - * twin_dfixed_t a = b'10100; - * twin_dfixed_t b = b'10000; - * exact a is 1*(2^(-4)) + 1*(2^(-6)) = 0.078125 - * exact b is 1*(2^(-4)) = 0.0625 - * - * The result of twin_dfixed_mul(a, b) is of type twin_dfixed_t - * (a * b) >> 8 = (b'101000000) >> 8 = b'1 - * exact result is 1*(2^(-8)) = 0.00390625 - */ +#define twin_fixed_to_xfixed(f) (((twin_xfixed_t) (f)) * 65536) +/* 28.4 product */ #define twin_sfixed_mul(a, b) \ (twin_sfixed_t)((((int32_t) (a)) * ((int32_t) (b))) >> 4) -#define twin_sfixed_div(a, b) \ - (twin_sfixed_t)((((int32_t) (a)) << 4) / ((int32_t) (b))) - -#define twin_dfixed_mul(a, b) \ - (twin_dfixed_t)((((int64_t) (a)) * ((int64_t) (b))) >> 8) -#define twin_dfixed_div(a, b) \ - (twin_dfixed_t)((((int64_t) (a)) << 8) / ((int64_t) (b))) /* 64-bit fixed-point multiplication and division * These require 128-bit intermediate results for full precision. @@ -307,12 +719,10 @@ static inline twin_xfixed_t _twin_xfixed_div_32bit(twin_xfixed_t a, #define twin_sfixed_to_double(f) ((double) (f) / 16.0) #define TWIN_SFIXED_ONE (0x10) -#define TWIN_SFIXED_HALF (0x08) #define TWIN_SFIXED_TOLERANCE (TWIN_SFIXED_ONE >> 2) #define TWIN_SFIXED_MIN (-0x7fff) #define TWIN_SFIXED_MAX (0x7fff) - #define TWIN_GFIXED_ONE (0x40) #define TWIN_XFIXED_ONE (0x100000000) @@ -321,10 +731,46 @@ static inline twin_xfixed_t _twin_xfixed_div_32bit(twin_xfixed_t a, */ #define twin_int_mult(a, b, t) \ ((t) = (a) * (b) + 0x80, ((((t) >> 8) + (t)) >> 8)) -#define twin_int_div(a, b) (((uint16_t) (a) * 255) / (b)) #define twin_get_8(v, i) ((uint16_t) (uint8_t) ((v) >> (i))) #define twin_sat(t) ((uint8_t) ((t) | (0 - ((t) >> 8)))) +/* + * Two channels at a time: 16-bit lanes hold the red/blue or alpha/green + * pair. A lane never overflows (255 * 255 + 0x80 + 0xff < 0x10000), so this + * rounds exactly like twin_int_mult does per channel. + */ +#define TWIN_LANES 0x00ff00ffu + +static inline uint32_t _twin_mul_lanes(uint32_t x, uint32_t m) +{ + uint32_t t = x * m + 0x00800080u; + return ((t + ((t >> 8) & TWIN_LANES)) >> 8) & TWIN_LANES; +} + +/* Saturate each lane to 0xff */ +static inline uint32_t _twin_sat_lanes(uint32_t x) +{ + uint32_t carry = (x >> 8) & 0x00010001u; + return (x | (carry * 0xff)) & TWIN_LANES; +} + +/* Porter-Duff over on premultiplied pixels: src + dst * (1 - src alpha) */ +static inline twin_argb32_t _twin_over(twin_argb32_t dst, twin_argb32_t src) +{ + if (!src) + return dst; + uint32_t a = (uint8_t) ~(src >> 24); + if (!a) + return src; + uint32_t rb = dst & TWIN_LANES, ag = (dst >> 8) & TWIN_LANES; + if (a != 0xff) { + rb = _twin_mul_lanes(rb, a); + ag = _twin_mul_lanes(ag, a); + } + return _twin_sat_lanes(rb + (src & TWIN_LANES)) | + _twin_sat_lanes(ag + ((src >> 8) & TWIN_LANES)) << 8; +} + #define twin_in(s, i, m, t) \ ((twin_argb32_t) twin_int_mult(twin_get_8(s, i), (m), (t)) << (i)) @@ -336,15 +782,6 @@ static inline twin_xfixed_t _twin_xfixed_div_32bit(twin_xfixed_t a, (((t) = twin_get_8(d, i) + twin_get_8(s, i)), (twin_argb32_t) twin_sat(t) \ << (i)) -#define _twin_add_ARGB(s, d, i, t) (((t) = (s) + twin_get_8(d, i))) -#define _twin_add(s, d, t) (((t) = (s) + (d))) -#define _twin_div(d, den, i, t) \ - (((t) = (d) / (den)), (t) = twin_get_8((t), 0), \ - (twin_argb32_t) twin_sat(t) << (i)) -#define _twin_sub_ARGB(s, d, i, t) (((t) = (s) - twin_get_8(d, i))) -#define _twin_sub(s, d, t) (((t) = (s) - (d))) -#define twin_put_8(d, i, t) (((t) = (d) << (i))) - #define twin_argb32_to_rgb16(s) \ ((((s) >> 3) & 0x001f) | (((s) >> 5) & 0x07e0) | (((s) >> 8) & 0xf800)) #define twin_rgb16_to_argb32(s) \ @@ -400,6 +837,9 @@ typedef struct _twin_xform { twin_coord_t width; twin_coord_t src_x; twin_coord_t src_y; + /* Where the clipped destination starts relative to the requested one; + * the transform maps unclipped destination offsets to the source. */ + twin_coord_t off_x, off_y; } twin_xform_t; /* twin_primitive.c */ @@ -447,18 +887,6 @@ typedef struct _twin_gpoint { /* Compositing function declarations - auto-generated */ #include "composite-decls.h" -twin_argb32_t *_twin_fetch_rgb16(twin_pixmap_t *pixmap, - int x, - int y, - int w, - twin_argb32_t *span); - -twin_argb32_t *_twin_fetch_argb32(twin_pixmap_t *pixmap, - int x, - int y, - int w, - twin_argb32_t *span); - /* * Geometry helper functions */ @@ -470,10 +898,6 @@ twin_dfixed_t _twin_distance_to_line_squared(twin_spoint_t *p, * Polygon stuff */ -/* - * Fixed point helper functions - */ -twin_sfixed_t _twin_sfixed_sqrt(twin_sfixed_t as); twin_xfixed_t _twin_xfixed_sqrt(twin_xfixed_t a); /* * Matrix stuff @@ -494,14 +918,6 @@ twin_sfixed_t _twin_matrix_dy(twin_matrix_t *m, twin_fixed_t dx, twin_fixed_t dy); -twin_fixed_t _twin_matrix_determinant(twin_matrix_t *matrix); - -twin_sfixed_t _twin_matrix_len(twin_matrix_t *m, - twin_fixed_t dx, - twin_fixed_t dy); - -twin_point_t _twin_matrix_expand(twin_matrix_t *matrix); - /* * Path stuff */ @@ -598,6 +1014,12 @@ void _twin_run_timeout(void); twin_time_t _twin_timeout_delay(void); +/* How long a backend may sleep waiting for input: not at all while the + * screen has damage to show, otherwise until the next timeout, at most + * @cap ms (@cap < 0: no cap, and -1 then means until input arrives). A + * NULL @screen is one that cannot show anything now: its damage waits. */ +twin_time_t _twin_idle_timeout(twin_screen_t *screen, twin_time_t cap); + void _twin_run_work(void); /* Event dispatch - internal use only @@ -623,6 +1045,12 @@ void _twin_box_init(twin_box_t *box, twin_box_dir_t dir, twin_widget_proc_t dispatch); +/* Flag @box and every widget inside it for repainting, e.g. when the + * window's pixels were released and have to be drawn again in full */ +void _twin_box_mark_paint(twin_box_t *box); +/* Fill @widget with its background, in its shape, over its ancestors' */ +void _twin_widget_paint(twin_widget_t *widget); + twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure); @@ -633,19 +1061,10 @@ void _twin_widget_init(twin_widget_t *widget, twin_widget_layout_t preferred, twin_widget_proc_t dispatch); -void _twin_widget_paint_shape(twin_widget_t *widget, - twin_shape_t shape, - twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom, - twin_fixed_t radius); - twin_dispatch_result_t _twin_widget_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure); - void _twin_widget_queue_paint(twin_widget_t *widget); void _twin_widget_queue_layout(twin_widget_t *widget); @@ -699,39 +1118,444 @@ typedef struct twin_backend { /* Initialize the backend */ twin_context_t *(*init)(int width, int height); - /* Configure the device */ - /* FIXME: Refine the parameter list */ - void (*configure)(twin_context_t *ctx); - + /* Wait for and dispatch input; false when the user asked to quit. + * Sleep with _twin_idle_timeout() rather than spin. */ bool (*poll)(twin_context_t *ctx); - /* Start the main loop with application initialization callback */ - void (*start)(twin_context_t *ctx, void (*init_callback)(twin_context_t *)); + /* Run the main loop after the application callback, for platforms + * that own the loop (the browser); NULL: twin_run() loops on + * twin_dispatch_once() */ + void (*start)(twin_context_t *ctx, void (*init_callback)(twin_screen_t *)); /* Device cleanup when drawing is done */ void (*exit)(twin_context_t *ctx); } twin_backend_t; +/* The backend linked into the program */ +extern const twin_backend_t g_twin_backend; + /* - * Visual effect stuff + * Window manager policy interface + * + * A policy decides window geometry, decorations, stacking, and how pointer + * events reach clients. Exactly one is built: wm-none.c (WM_NONE, the + * minimal one), wm-desktop.c (WM_DESKTOP, classic) or wm-mobile.c + * (WM_MOBILE, embedded). It defines the hooks below and window.c, + * screen.c and wm.c call them directly; a hook a policy has no use for is + * an inline no-op here, so the call compiles away. Policies never free a + * window. */ +#if defined(CONFIG_WM_DESKTOP) + defined(CONFIG_WM_MOBILE) + \ + defined(CONFIG_WM_NONE) != \ + 1 +#error \ + "Select exactly one of CONFIG_WM_DESKTOP, CONFIG_WM_MOBILE, CONFIG_WM_NONE" +#endif + +/* Title bars: decorate(), hit() and activate() (desktop) */ +#if defined(CONFIG_WM_DESKTOP) +#define TWIN_WM_DECORATES 1 +#else +#define TWIN_WM_DECORATES 0 +#endif +/* System bars and a work area: filter() and work_area() (mobile) */ +#if defined(CONFIG_WM_MOBILE) +#define TWIN_WM_BARS 1 +#else +#define TWIN_WM_BARS 0 +#endif + +/* Bytes of policy state that follow the screen in its allocation, zeroed; + * _twin_wm_state() finds them */ +extern const size_t _twin_wm_state_size; +#define _twin_wm_state(screen) ((void *) ((twin_screen_t *) (screen) + 1)) + +/* The screen was created / is going away. fini() runs before the windows + * are destroyed and must let go of them: from then on (screen->closing) + * window.c bypasses the policy. */ +void _twin_wm_init(twin_screen_t *screen); +void _twin_wm_fini(twin_screen_t *screen); + +/* Compute pixmap geometry (screen coordinates) and decoration border */ +void _twin_wm_layout(twin_window_t *window, + twin_rect_t *geom, + twin_rect_t *border); + +/* Application mapped / unmapped a window. On destroy, the policy may take + * ownership of window->pixmap (setting it to NULL) to animate the departure + * after the window object is gone. */ +void _twin_wm_map(twin_window_t *window); +void _twin_wm_unmap(twin_window_t *window, bool destroying); + +/* Deliver an event targeted at a window */ +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event); + +/* Re-place the stack after a mapped window changed style or opacity, or the + * system bars, the inset or the screen size changed. Every window has been + * re-laid out already. */ +#if defined(CONFIG_WM_NONE) +static inline void _twin_wm_refresh(twin_screen_t *screen) +{ + (void) screen; +} +#else +void _twin_wm_refresh(twin_screen_t *screen); +#endif + +#if TWIN_WM_DECORATES +/* Paint decorations into window->pixmap */ +void _twin_wm_decorate(twin_window_t *window); +/* Pointer hit test in screen coordinates (others: the pixmap's shown + * rows, see _twin_window_hit) */ +bool _twin_wm_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y); +/* Explicit activation request (twin_window_activate) */ +void _twin_wm_activate(twin_window_t *window); +#else +static inline void _twin_wm_decorate(twin_window_t *window) +{ + (void) window; +} +static inline void _twin_wm_activate(twin_window_t *window) +{ + _twin_wm_map(window); +} +#endif + +#if TWIN_WM_BARS +/* Gesture hook: sees every pointer event (motion, button down/up) before it + * is routed to a window; true consumes it. Other kinds bypass it. */ +bool _twin_wm_filter(twin_screen_t *screen, twin_event_t *event); +/* Shrink @area (preset to the whole screen) to what application windows may + * cover, e.g. minus the system bars */ +void _twin_wm_work_area(const twin_screen_t *screen, twin_rect_t *area); +#else +static inline bool _twin_wm_filter(twin_screen_t *screen, twin_event_t *event) +{ + (void) screen; + (void) event; + return false; +} +#endif + +/* window.c services used by policies */ +void _twin_window_relayout(twin_window_t *window); +/* Run draw() for the pending damage on the next pass (twin_window_damage() + * does this itself) */ +void twin_window_queue_paint(twin_window_t *window); +bool _twin_window_deliver(twin_window_t *window, twin_event_t *event); +void _twin_window_evict(twin_window_t *window); +bool _twin_window_restore(twin_window_t *window); +#if defined(CONFIG_WM_MOBILE) +bool _twin_window_is_bar(const twin_window_t *window); +#else +/* Only the mobile window manager has system bars */ +static inline bool _twin_window_is_bar(const twin_window_t *window) +{ + (void) window; + return false; +} +#endif +/* On screen with its pixels (not hidden, not evicted) */ +bool _twin_window_shown(const twin_window_t *window); +/* Mapped window the policy stacks and focuses (not Plain, not a bar) */ +bool _twin_window_managed(const twin_window_t *window); +void _twin_window_raise(twin_window_t *window); +/* + * Client callbacks run while the screen is busy (an event, a relayout). A + * window destroyed then is torn down and unlinked at once, but its struct stays + * readable (window->dead) until the outermost busy section ends, so code + * holding the pointer can check it. + */ +void _twin_screen_busy(twin_screen_t *screen); +void _twin_screen_idle(twin_screen_t *screen); + +/* screen.c: route a pointer event to windows (grab, hover, delivery), + * skipping the policy's gesture filter */ +bool _twin_screen_route(twin_screen_t *screen, twin_event_t *event); + +/* wm.c: re-lay out every window after the screen, the bars or the inset + * changed, then let the policy re-place the stack */ +void _twin_screen_relayout(twin_screen_t *screen); + +/* + * Pixmap internals + */ + +/* Screen-space rectangle touched by the pixmap, including its shadow */ +twin_rect_t _twin_pixmap_extents(const twin_pixmap_t *pixmap); + +/* Show only the top @rows rows on scan-out and to the pointer (the rest + * keeps its pixels); pixmap->height shows everything. Used by the desktop + * policy to iconify a window down to its title bar. */ +void _twin_pixmap_set_scan_height(twin_pixmap_t *pixmap, twin_coord_t rows); + +/* Damage everything the pixmap covers on screen, shadow included */ +void _twin_pixmap_damage_extents(twin_pixmap_t *pixmap); + +/* + * Add @r to a rectangle list of capacity @max. Rectangles merge only when + * their union costs no more pixels than the pair; a full list folds @r into + * the entry that grows least. Shared by screen and window damage. + */ +void _twin_rect_list_add(twin_rect_t *list, int *n, int max, twin_rect_t r); + +/* Unlink from the display list without dropping focus/pointer state */ +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); + +/* 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, + twin_coord_t height); + +static inline bool _twin_pixmap_is_stub(const twin_pixmap_t *pixmap) +{ + return pixmap->p.v == NULL; +} + +/* Wrap caller-owned pixels in a pixmap header without allocating */ +void _twin_pixmap_init(twin_pixmap_t *pixmap, + twin_format_t format, + twin_coord_t width, + twin_coord_t height, + twin_coord_t stride, + void *pixels); +void _twin_pixmap_init_a8(twin_pixmap_t *pixmap, + twin_coord_t width, + twin_coord_t height, + twin_coord_t stride, + void *pixels); + +/* + * Screen compositor primitives (screen-ops.c, renderer independent) + */ +void _twin_screen_blend_argb32(twin_argb32_t *dst, + const twin_argb32_t *src, + twin_a8_t alpha, + int width); +void _twin_screen_blend_rgb16(twin_argb32_t *dst, + const twin_rgb16_t *src, + twin_a8_t alpha, + int width); +void _twin_screen_fill(twin_argb32_t *dst, twin_argb32_t color, int width); +/* Opaque @src at constant @alpha over the solid color @under, written to + * @dst: the result of filling with @under and then blending, with half + * the multiplies (@src must be opaque, @alpha below 255) */ +void _twin_screen_blend_onto_argb32(twin_argb32_t *dst, + const twin_argb32_t *src, + twin_a8_t alpha, + twin_argb32_t under, + int width); +void _twin_screen_blend_onto_rgb16(twin_argb32_t *dst, + const twin_rgb16_t *src, + twin_a8_t alpha, + twin_argb32_t under, + int width); -#if defined(CONFIG_DROP_SHADOW) -#define TWIN_SHADOW_SOFTNESS_TAIL (((CONFIG_SHADOW_BLUR) + 1) / 2) -#define TWIN_SHADOW_FADE_EXTENT \ - ((CONFIG_SHADOW_FADE_TAIL > TWIN_SHADOW_SOFTNESS_TAIL) \ - ? CONFIG_SHADOW_FADE_TAIL \ - : TWIN_SHADOW_SOFTNESS_TAIL) -#define TWIN_SHADOW_EXTENT_X \ - (CONFIG_HORIZONTAL_OFFSET + TWIN_SHADOW_FADE_EXTENT) -#define TWIN_SHADOW_EXTENT_Y (CONFIG_VERTICAL_OFFSET + TWIN_SHADOW_FADE_EXTENT) /* - * Render a CSS-style drop shadow mask onto the pixmap's shadow region. - * Offsets and blur are compile-time constants (CONFIG_HORIZONTAL_OFFSET, - * CONFIG_VERTICAL_OFFSET, CONFIG_SHADOW_BLUR). Requires ARGB32 format. + * Coverage cache (cache.c) + * + * Entries hold A8 coverage plus placement. Lookups bump LRU order; inserts + * evict least recently used entries until the budget is honored. */ -void twin_shadow_border(twin_pixmap_t *shadow, twin_argb32_t color); +typedef struct _twin_cache_entry { + uint64_t key; /* first, so 32-bit targets need no padding after it */ + struct _twin_cache_entry *hnext; /* hash chain */ + struct _twin_cache_entry *prev, *next; /* LRU list */ + uint32_t size; /* bytes charged to the budget */ + twin_coord_t width, height, stride; + twin_coord_t left, top; /* placement relative to the key's origin */ + twin_fixed_t advance; /* glyph advance (glyph entries only) */ + uint8_t data[]; /* stride * height bytes of A8 coverage */ +} twin_cache_entry_t; + +/* Row stride of A8 coverage: rows start on 4-byte boundaries */ +static inline twin_coord_t _twin_a8_stride(twin_coord_t width) +{ + return (twin_coord_t) ((width + 3) & ~3); +} + +/* Zeroed entry outside the cache (caller frees); the cache uses it too */ +twin_cache_entry_t *_twin_cache_entry_alloc(twin_coord_t width, + twin_coord_t height); +twin_cache_entry_t *_twin_cache_lookup(uint64_t key); +twin_cache_entry_t *_twin_cache_insert(uint64_t key, + twin_coord_t width, + twin_coord_t height); +bool _twin_cache_enabled(void); + +/* Largest single entry admitted to the cache, in bytes */ +#ifndef CONFIG_CACHE_MAX_ENTRY +#define CONFIG_CACHE_MAX_ENTRY 4096 #endif +#ifndef CONFIG_CACHE_SIZE +#define CONFIG_CACHE_SIZE 24 +#endif + +/* 64-bit FNV-1a */ +#define TWIN_HASH_INIT 0xcbf29ce484222325ULL +static inline uint64_t _twin_hash(uint64_t h, const void *data, size_t len) +{ + const uint8_t *p = data; + while (len--) { + h ^= *p++; + h *= 0x100000001b3ULL; + } + return h; +} + +/* Set up a caller-owned path (inline storage, default state) / release it */ +void _twin_path_init(twin_path_t *path); +void _twin_path_fini(twin_path_t *path); + +/* Bounds of points [first, npoints), as twin_path_bounds() */ +void _twin_path_bounds_from(const twin_path_t *path, + int first, + twin_rect_t *rect); + +int _twin_utf8_to_ucs4(const char *src, twin_ucs4_t *dst); + +/* + * Test and diagnostics hooks (not part of the public API) + */ + +#if defined(CONFIG_MEMORY_STATS) +/* Make the next @n tracked allocations fail (out-of-memory tests) */ +extern unsigned int _twin_alloc_fail_next; +#endif + +/* + * Library-internal helpers: used across src/, not part of the public API + */ + +twin_angle_t twin_acos(twin_fixed_t x); + +/** + * Calculate square root of fixed-point number + * @a : Input value in fixed-point format + * + * Return Square root in fixed-point format + */ +twin_fixed_t twin_fixed_sqrt(twin_fixed_t a); + +bool twin_matrix_is_identity(twin_matrix_t *m); + +void twin_matrix_multiply(twin_matrix_t *result, + const twin_matrix_t *a, + const twin_matrix_t *b); + +void twin_matrix_translate(twin_matrix_t *m, twin_fixed_t tx, twin_fixed_t ty); + +void twin_path_append(twin_path_t *dst, twin_path_t *src); + +void twin_path_arc_circle(twin_path_t *path, + bool large_arc, + bool sweep, + twin_fixed_t radius, + twin_fixed_t cur_x, + twin_fixed_t cur_y, + twin_fixed_t target_x, + twin_fixed_t target_y); + +void twin_path_lozenge(twin_path_t *path, + twin_fixed_t x, + twin_fixed_t y, + twin_fixed_t w, + twin_fixed_t h); + +/** + * Add filled outline of Unicode character to path + * @path : Path to add character to + * @ucs4 : Unicode character to add + * + * Adds character outline to path for filled text rendering. + */ +void twin_path_ucs4(twin_path_t *path, twin_ucs4_t ucs4); + +/** + * Get advance width of Unicode character + * @path : Path with current font settings + * @ucs4 : Unicode character + * + * Return Character advance width in fixed-point format + */ +twin_fixed_t twin_width_ucs4(twin_path_t *path, twin_ucs4_t ucs4); + +/** + * Create pixmap using existing pixel buffer + * @format : Pixel format + * @width : Width in pixels + * @height : Height in pixels + * @stride : Row stride in bytes + * @pixels : Existing pixel buffer (not copied) + * + * Return Pixmap wrapper for existing buffer, or NULL on failure + */ +twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, + twin_coord_t width, + twin_coord_t height, + twin_coord_t stride, + twin_pointer_t pixels); + +void twin_pixmap_damage(twin_pixmap_t *pixmap, + twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_coord_t bottom); + +bool twin_pixmap_dispatch(twin_pixmap_t *pixmap, twin_event_t *event); + +void twin_pixmap_get_origin(twin_pixmap_t *pixmap, + twin_coord_t *ox, + twin_coord_t *oy); + +void twin_pixmap_set_clip(twin_pixmap_t *pixmap, twin_rect_t clip); + +void twin_pixmap_set_origin(twin_pixmap_t *pixmap, + twin_coord_t ox, + twin_coord_t oy); + +static inline int twin_ucs_page(uint32_t ucs4) +{ + return ucs4 >> UCS_PAGE_SHIFT; +} + +static inline int twin_ucs_char_in_page(uint32_t ucs4) +{ + return ucs4 & (UCS_PER_PAGE - 1); +} + +/* 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 { + TwinIconMenu, + TwinIconIconify, + TwinIconRestore, + TwinIconClose, + TwinIconResize, +} twin_icon_t; + +void twin_icon_draw(twin_pixmap_t *pixmap, + twin_icon_t icon, + twin_matrix_t matrix); + +/* Replace the monotonic clock; NULL restores the system clock */ +void _twin_set_time_source(twin_time_t (*now)(void)); + +/* Debug switches used by differential tests: + * NO_OCCLUSION composites every layer at every pixel; + * NO_ANIMATION makes window manager transitions instant; + * ROW_FALLBACK takes the plain bottom-up path that rows with too many + * layers use, so it can be compared with the culled one. */ +#define TWIN_DEBUG_NO_OCCLUSION 0x1 +#define TWIN_DEBUG_NO_ANIMATION 0x2 +#define TWIN_DEBUG_ROW_FALLBACK 0x4 +extern unsigned _twin_debug; /* utility */ @@ -751,6 +1575,30 @@ static inline int twin_clzll(uint64_t v) return 63 - leading_zero; return 64; /* undefined behavior */ } +#elif defined(__riscv) && !defined(__riscv_zbb) +/* Without Zbb there is no count-leading-zeros instruction and + * __builtin_clz becomes a libgcc call; a binary search of shifts is + * cheaper. Like the builtin, @v must be nonzero. */ +static inline int twin_clz(uint32_t v) +{ + int n = 0; + if (!(v & 0xffff0000u)) + n += 16, v <<= 16; + if (!(v & 0xff000000u)) + n += 8, v <<= 8; + if (!(v & 0xf0000000u)) + n += 4, v <<= 4; + if (!(v & 0xc0000000u)) + n += 2, v <<= 2; + if (!(v & 0x80000000u)) + n += 1; + return n; +} +static inline int twin_clzll(uint64_t v) +{ + uint32_t hi = (uint32_t) (v >> 32); + return hi ? twin_clz(hi) : 32 + twin_clz((uint32_t) v); +} #elif defined(__GNUC__) || defined(__clang__) static inline int twin_clz(uint32_t v) { @@ -791,6 +1639,14 @@ static inline int twin_clzll(uint64_t v) extern const uint8_t _twin_cursor_default[]; +/* The default arrow cursor, hotspot at its tip (CONFIG_CURSOR) */ +twin_pixmap_t *_twin_make_cursor(void); +/* @pixmap becomes the screen's: it is destroyed with the screen */ +void twin_screen_set_cursor(twin_screen_t *screen, + twin_pixmap_t *pixmap, + twin_coord_t hotspot_x, + twin_coord_t hotspot_y); + /* Pattern Matching for C macros. * https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms */ @@ -819,20 +1675,31 @@ void twin_path_convolve(twin_path_t *dest, twin_path_t *pen, twin_scratch_t *scratch); -void twin_premultiply_alpha(twin_pixmap_t *px); - -void twin_cover(twin_pixmap_t *dst, - twin_argb32_t color, - twin_coord_t x, - twin_coord_t y, - twin_coord_t width); - void twin_fill_path(twin_pixmap_t *pixmap, twin_path_t *path, twin_coord_t dx, twin_coord_t dy, twin_scratch_t *scratch); +/* + * Rasterize a mask @total rows tall through @band, an A8 pixmap of fewer + * rows: whenever the scan leaves a band it calls @flush with the band's + * first row and height, then clears the band for the next. Every band is + * flushed, in order, so the result matches one full-height mask. + */ +typedef struct { + twin_coord_t total; + void (*flush)(void *ctx, twin_coord_t top, twin_coord_t rows); + void *ctx; +} twin_band_t; + +void twin_fill_path_banded(twin_pixmap_t *band, + const twin_band_t *bands, + twin_path_t *path, + twin_coord_t dx, + twin_coord_t dy, + twin_scratch_t *scratch); + void twin_composite_path(twin_pixmap_t *dst, twin_operand_t *src, twin_coord_t src_x, @@ -851,35 +1718,26 @@ void twin_composite_stroke(twin_pixmap_t *dst, twin_scratch_t * scratch); /* Internal window management helpers */ -void _twin_window_style_size(twin_window_style_t style, twin_rect_t *size); -bool _twin_window_valid_range(twin_window_t *window, - twin_coord_t x, - twin_coord_t y); +bool _twin_window_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y); bool _twin_window_dispatch(twin_window_t *window, twin_event_t *event); -/* Internal event handling */ -void twin_event_enqueue(const twin_event_t *event); - /* Internal pixmap operations */ -twin_pointer_t twin_pixmap_pointer(twin_pixmap_t *pixmap, - twin_coord_t x, - twin_coord_t y); +/* Address of pixel (x, y): inline, as every span loop starts with it */ +static inline twin_pointer_t twin_pixmap_pointer(twin_pixmap_t *pixmap, + twin_coord_t x, + twin_coord_t y) +{ + twin_pointer_t p; + p.b = pixmap->p.b + (int32_t) y * pixmap->stride + + (int32_t) x * twin_bytes_per_pixel(pixmap->format); + return p; +} void twin_pixmap_reset_xform_cache(twin_pixmap_t *pixmap); void twin_pixmap_origin_to_clip(twin_pixmap_t *pixmap); -void twin_pixmap_offset(twin_pixmap_t *pixmap, - twin_coord_t offx, - twin_coord_t offy); /* Internal screen operations */ void twin_screen_enable_update(twin_screen_t *screen); void twin_screen_disable_update(twin_screen_t *screen); -void twin_screen_register_damaged(twin_screen_t *screen, - void (*damaged)(void *), - void *closure); - -/* Internal widget operations */ -void twin_widget_children_paint(twin_box_t *box); -void twin_custom_widget_destroy(twin_custom_widget_t *custom); /* Reusable path acquisition -- avoids malloc/free per draw call */ twin_path_t *_twin_path_acquire(twin_screen_t *screen); @@ -930,30 +1788,25 @@ static inline bool twin_pointer_valid(const void *ptr) * callbacks are still queued. */ -/* Maximum number of tracked closures */ -#define TWIN_MAX_CLOSURES 1024 +/* Closures tracked without touching the heap; beyond this the table grows + * on demand and returns to the inline slots once it drains */ +#define TWIN_CLOSURE_INLINE 16 -/* Closure tracking entry */ typedef struct _twin_closure_entry { - void *closure; /* Closure pointer */ - uint32_t ref_count; /* Reference count */ - bool marked_for_free; /* Closure is being freed */ + void *closure; + uint32_t ref_count : 31; + uint32_t marked_for_free : 1; /* being freed: no new references */ } twin_closure_entry_t; -/* Closure tracking table */ typedef struct _twin_closure_tracker { - twin_closure_entry_t entries[TWIN_MAX_CLOSURES]; - int count; - bool initialized; /* Track initialization status */ - /* Mutex would go here for thread safety if needed */ + twin_closure_entry_t *entries; /* inline, or heap once grown */ + int count, capacity; + twin_closure_entry_t inline_entries[TWIN_CLOSURE_INLINE]; } twin_closure_tracker_t; /* Global closure tracker instance */ extern twin_closure_tracker_t _twin_closure_tracker; -/* Initialize closure tracking system */ -void _twin_closure_tracker_init(void); - /* Register a closure with the tracking system */ bool _twin_closure_register(void *closure); @@ -1040,10 +1893,9 @@ void _twin_free(void *ptr, const char *file, int line); /* * Lazy widget extension accessors. * - * The optional fields (callback, callback_data, want_focus) live in a - * separately allocated twin_widget_ext_t block. Widgets that never - * register a callback or request focus keep ext == NULL and pay only - * one pointer. + * The optional fields (callback, callback_data) live in a separately + * allocated twin_widget_ext_t block. Widgets that never register a + * callback keep ext == NULL and pay only one pointer. */ /* Allocate the extension block if absent. Returns ext or NULL on OOM. */ @@ -1065,9 +1917,104 @@ static inline void *_twin_widget_callback_data(const twin_widget_t *w) return w->ext ? w->ext->callback_data : NULL; } -static inline bool _twin_widget_want_focus(const twin_widget_t *w) -{ - return w->ext ? w->ext->want_focus : false; -} +/* + * Interfaces for the library, its backends, tests and tools; applications + * use include/twin.h only + */ + +/* Work items run once per pass, lowest priority value first: layout, + * then painting, then the backend's redisplay, so a pass shows what it + * painted instead of leaving it for the next */ +#define TWIN_WORK_LAYOUT 0 +#define TWIN_WORK_PAINT 1 +#define TWIN_WORK_REDISPLAY 2 + +twin_work_t *twin_set_work(twin_work_proc_t work_proc, + int priority, + void *closure); + +void twin_clear_work(twin_work_t *work); + +void twin_pixmap_show(twin_pixmap_t *pixmap, + twin_screen_t *screen, + twin_pixmap_t *higher); + +void twin_pixmap_hide(twin_pixmap_t *pixmap); + +void twin_pixmap_move(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y); + +/** + * Set the constant opacity applied when the pixmap is scanned out + */ +void twin_pixmap_set_opacity(twin_pixmap_t *pixmap, twin_a8_t opacity); + +/** + * Attach a procedural drop shadow + * @pixmap : Pixmap casting the shadow + * @dx, dy : Offset of the shadow + * @blur : Blur radius in pixels, 0 removes the shadow + * @alpha : Peak opacity of the shadow + */ +void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, + twin_coord_t dx, + twin_coord_t dy, + twin_coord_t blur, + twin_a8_t alpha); + +void twin_pixmap_enable_update(twin_pixmap_t *pixmap); + +void twin_pixmap_disable_update(twin_pixmap_t *pixmap); + +void twin_pixmap_reset_clip(twin_pixmap_t *pixmap); + +/* Low level: route keys to @pixmap without telling the window manager. + * Applications raise and focus a window with twin_window_activate(). */ +void twin_screen_set_active(twin_screen_t *screen, twin_pixmap_t *pixmap); + +/* Request widget repaint */ +void twin_widget_queue_paint(twin_widget_t *widget); + +/** + * Get total width of UTF-8 string + * @path : Path with current font settings + * @string : UTF-8 encoded string + * + * Return Total string width in fixed-point format + */ +twin_fixed_t twin_width_utf8(twin_path_t *path, const char *string); + +/** Font ascent and descent (both positive) at @size */ +void twin_font_extents(twin_font_t *font, + twin_fixed_t size, + twin_fixed_t *ascent, + twin_fixed_t *descent); + +/** + * Check if font contains specified Unicode character + * @font : Font to check + * @ucs4 : Unicode character code + * + * Return True if character is available in font + */ +bool twin_has_ucs4(twin_font_t *font, twin_ucs4_t ucs4); + +/** Select the font used by twin_path_utf8() and its measuring calls */ +void twin_path_set_font(twin_path_t *path, twin_font_t *font); + +void twin_path_bounds(twin_path_t *path, twin_rect_t *rect); + +/** Apply an easing curve to linear progress @t in [0, TWIN_FIXED_ONE] */ +twin_fixed_t twin_ease(twin_ease_t ease, twin_fixed_t t); + +/** Jump a tween to its end state (step callback sees TWIN_FIXED_ONE) */ +void twin_tween_finish(twin_tween_t *tween); + +void twin_cache_flush(void); + +void twin_cache_set_budget(size_t bytes); + +void twin_memory_reset_peak(void); + +twin_format_t twin_pixmap_format(const twin_pixmap_t *pixmap); #endif /* _TWIN_PRIVATE_H_ */ diff --git a/src/widget.c b/src/widget.c index e1922844..b4146f1b 100644 --- a/src/widget.c +++ b/src/widget.c @@ -7,75 +7,50 @@ #include +#include + #include "twin_private.h" -static twin_path_t *_twin_path_shape(twin_shape_t shape, - twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom, - twin_fixed_t radius) +/* What the ancestors' rectangular backgrounds leave under @widget */ +static twin_argb32_t _twin_widget_under(const twin_widget_t *widget) { - twin_path_t *path = twin_path_create(); - if (!path) - return NULL; - - twin_fixed_t x = twin_int_to_fixed(left); - twin_fixed_t y = twin_int_to_fixed(top); - twin_fixed_t w = twin_int_to_fixed(right - left); - twin_fixed_t h = twin_int_to_fixed(bottom - top); - - switch (shape) { - case TwinShapeRectangle: - twin_path_rectangle(path, x, y, w, h); - break; - case TwinShapeRoundedRectangle: - twin_path_rounded_rectangle(path, x, h, w, y, radius, radius); - break; - case TwinShapeLozenge: - twin_path_lozenge(path, x, y, w, h); - break; - case TwinShapeTab: - twin_path_tab(path, x, y, w, h, radius, radius); - break; - case TwinShapeEllipse: - twin_path_ellipse(path, x + w / 2, y + h / 2, w / 2, h / 2); - break; - } - return path; + if (!widget->parent) + return 0; + const twin_widget_t *parent = &widget->parent->widget; + twin_argb32_t under = _twin_widget_under(parent); + if (parent->shape != TwinShapeRectangle) + return under; + return _twin_over(under, parent->background); } -void _twin_widget_paint_shape(twin_widget_t *widget, - twin_shape_t shape, - twin_coord_t left, - twin_coord_t top, - twin_coord_t right, - twin_coord_t bottom, - twin_fixed_t radius) +/* Fill the widget with its background, in its shape, over what its + * ancestors painted there: a repaint erases the old content without + * punching a hole through the ancestors' backgrounds */ +void _twin_widget_paint(twin_widget_t *widget) { twin_pixmap_t *pixmap = widget->window->pixmap; + twin_coord_t w = _twin_widget_width(widget); + twin_coord_t h = _twin_widget_height(widget); + twin_argb32_t under = _twin_widget_under(widget); - if (shape == TwinShapeRectangle) - twin_fill(pixmap, widget->background, TWIN_SOURCE, left, top, right, - bottom); - else { - twin_path_t *path = - _twin_path_shape(shape, left, top, right, bottom, radius); - if (path) { - twin_paint_path(pixmap, widget->background, path); - twin_path_destroy(path); - } + if (widget->shape == TwinShapeRectangle) { + twin_fill(pixmap, _twin_over(under, widget->background), TWIN_SOURCE, 0, + 0, w, h); + return; } + twin_fill(pixmap, under, TWIN_SOURCE, 0, 0, w, h); + twin_path_t *path = twin_path_create(); + if (!path) + return; + twin_fixed_t fw = twin_int_to_fixed(w), fh = twin_int_to_fixed(h); + if (widget->shape == TwinShapeLozenge) + twin_path_lozenge(path, 0, 0, fw, fh); + else + twin_path_ellipse(path, fw / 2, fh / 2, fw / 2, fh / 2); + twin_paint_path(pixmap, widget->background, path); + twin_path_destroy(path); } -static void _twin_widget_paint(twin_widget_t *widget) -{ - _twin_widget_paint_shape(widget, widget->shape, 0, 0, - _twin_widget_width(widget), - _twin_widget_height(widget), widget->radius); -} - - twin_dispatch_result_t _twin_widget_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure) @@ -101,6 +76,10 @@ twin_dispatch_result_t _twin_widget_dispatch(twin_widget_t *widget, widget->paint = false; break; case TwinEventDestroy: + /* Last call to the application callback: it may release whatever + * it attached as callback data */ + if (widget->ext && widget->ext->callback) + widget->ext->callback(widget, event, widget->ext->callback_data); twin_free(widget->ext); widget->ext = NULL; break; @@ -139,7 +118,6 @@ void _twin_widget_init(twin_widget_t *widget, widget->preferred = preferred; widget->handler = handler; widget->shape = TwinShapeRectangle; - widget->radius = twin_int_to_fixed(12); } void _twin_widget_queue_paint(twin_widget_t *widget) @@ -213,300 +191,159 @@ void _twin_widget_bevel(twin_widget_t *widget, twin_fixed_t b, bool down) } } -void twin_widget_children_paint(twin_box_t *box) +/* The root widget fills the whole client area, so its background decides + * whether the window is opaque: then the compositor skips what lies + * underneath and the window manager may release covered windows. */ +static void _twin_widget_update_opaque(twin_widget_t *widget) { - for (twin_widget_t *child = box->children; child; child = child->next) - _twin_widget_queue_paint(child); + if (widget->parent) + return; + bool opaque = (widget->background >> 24) == 0xff && + widget->shape == TwinShapeRectangle; + if (widget->window->opaque != opaque) + twin_window_set_opaque(widget->window, opaque); } -twin_widget_t *twin_widget_create(twin_box_t *parent, - twin_argb32_t background, - twin_coord_t width, - twin_coord_t height, - twin_stretch_t stretch_width, - twin_stretch_t stretch_height) +/* The background or shape of @widget changed */ +static void _twin_widget_restyle(twin_widget_t *widget) { - twin_widget_t *widget = twin_malloc(sizeof(twin_widget_t)); - if (!widget) - return NULL; - - twin_widget_layout_t preferred = { - .width = width, - .height = height, - .stretch_width = stretch_width, - .stretch_height = stretch_height, - }; - _twin_widget_init(widget, parent, 0, preferred, _twin_widget_dispatch); - widget->background = background; - return widget; + _twin_widget_update_opaque(widget); + _twin_widget_queue_paint(widget); + /* Every descendant fills over the box's background: repaint them all + * (after queueing, which stops at a widget already marked) */ + if (widget->handler == _twin_box_dispatch || + widget->handler == _twin_toplevel_dispatch) + _twin_box_mark_paint((twin_box_t *) widget); } void twin_widget_set(twin_widget_t *widget, twin_argb32_t background) { widget->background = background; - _twin_widget_queue_paint(widget); + _twin_widget_restyle(widget); } -twin_fixed_t twin_widget_width(twin_widget_t *widget) +void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape) { - return _twin_widget_width(widget); + widget->shape = shape; + _twin_widget_restyle(widget); } -twin_fixed_t twin_widget_height(twin_widget_t *widget) +void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source) { - return _twin_widget_height(widget); + widget->copy_geom = source; + _twin_widget_queue_layout(widget); } -void twin_widget_queue_paint(twin_widget_t *widget) +static twin_coord_t twin_widget_width(twin_widget_t *widget) { - _twin_widget_queue_paint(widget); + return _twin_widget_width(widget); } -twin_widget_t *twin_widget_create_with_handler(twin_box_t *parent, - twin_argb32_t background, - twin_coord_t width, - twin_coord_t height, - twin_stretch_t stretch_width, - twin_stretch_t stretch_height, - twin_widget_proc_t handler) +static twin_coord_t twin_widget_height(twin_widget_t *widget) { - twin_widget_t *widget = twin_malloc(sizeof(twin_widget_t)); - if (!widget) - return NULL; - - twin_widget_layout_t preferred = { - .width = width, - .height = height, - .stretch_width = stretch_width, - .stretch_height = stretch_height, - }; - _twin_widget_init(widget, parent, 0, preferred, handler); - widget->background = background; - return widget; + return _twin_widget_height(widget); } -/* Map to store custom widget associations */ -typedef struct _custom_widget_map { - twin_widget_t *widget; - twin_custom_widget_t *custom; - twin_widget_proc_t user_dispatch; - struct _custom_widget_map *next; -} custom_widget_map_t; - -static custom_widget_map_t *custom_widget_map = NULL; - -static void register_custom_widget(twin_widget_t *widget, - twin_custom_widget_t *custom, - twin_widget_proc_t user_dispatch) +void twin_widget_queue_paint(twin_widget_t *widget) { - custom_widget_map_t *entry = twin_malloc(sizeof(custom_widget_map_t)); - if (!entry) - return; - entry->widget = widget; - entry->custom = custom; - entry->user_dispatch = user_dispatch; - entry->next = custom_widget_map; - custom_widget_map = entry; + _twin_widget_queue_paint(widget); } -static twin_custom_widget_t *find_custom_widget(twin_widget_t *widget) -{ - custom_widget_map_t *entry = custom_widget_map; - while (entry) { - if (entry->widget == widget) - return entry->custom; - entry = entry->next; - } - return NULL; -} +/* The application's data follows the custom widget in its allocation */ +#define CUSTOM_DATA_OFFSET \ + ((sizeof(twin_custom_widget_t) + _Alignof(max_align_t) - 1) & \ + ~(_Alignof(max_align_t) - 1)) -static void unregister_custom_widget(twin_widget_t *widget) +static twin_dispatch_result_t _twin_custom_dispatch(twin_widget_t *widget, + twin_event_t *event, + void *closure) { - custom_widget_map_t **prev = &custom_widget_map; - custom_widget_map_t *entry = custom_widget_map; + twin_custom_widget_t *custom = (twin_custom_widget_t *) widget; - if (!widget) - return; - - while (entry) { - if (entry->widget == widget) { - *prev = entry->next; - twin_free(entry); - return; - } - prev = &entry->next; - entry = entry->next; - } -} - -static twin_dispatch_result_t custom_widget_dispatch(twin_widget_t *widget, - twin_event_t *event, - void *closure) -{ - /* Handle destroy events specially to ensure proper cleanup order */ + /* The application releases what its data points to first; the box + * frees the widget, data included, afterwards */ if (event->kind == TwinEventDestroy) { - /* Find and call user dispatch first if it exists */ - custom_widget_map_t *entry = custom_widget_map; - while (entry) { - if (entry->widget == widget) { - if (entry->user_dispatch) - entry->user_dispatch(widget, event, closure); - break; - } - entry = entry->next; - } - - /* Unregister from custom widget map before base destruction */ - unregister_custom_widget(widget); - - /* Now call base widget dispatch to complete destruction */ + if (custom->handler) + custom->handler(custom, event); return _twin_widget_dispatch(widget, event, closure); } - - /* First call the base widget dispatch to handle standard widget behavior */ twin_dispatch_result_t result = _twin_widget_dispatch(widget, event, closure); - if (result == TwinDispatchDone) + if (result == TwinDispatchDone || !custom->handler) return result; - - /* Then call the user's custom dispatch if any */ - custom_widget_map_t *entry = custom_widget_map; - while (entry) { - if (entry->widget == widget) { - if (entry->user_dispatch) - return entry->user_dispatch(widget, event, closure); - break; - } - entry = entry->next; - } - return TwinDispatchContinue; + return custom->handler(custom, event); } twin_custom_widget_t *twin_custom_widget_create(twin_box_t *parent, twin_argb32_t background, twin_coord_t width, twin_coord_t height, - twin_stretch_t hstretch, - twin_stretch_t vstretch, - twin_widget_proc_t handler, + twin_custom_proc_t handler, size_t data_size) { - twin_custom_widget_t *custom = twin_malloc(sizeof(twin_custom_widget_t)); - if (!custom) + if (data_size > SIZE_MAX - CUSTOM_DATA_OFFSET) return NULL; - - if (data_size > 0) { - custom->data = twin_malloc(data_size); - if (!custom->data) { - twin_free(custom); - return NULL; - } - memset(custom->data, 0, data_size); - } else { - custom->data = NULL; - } - - custom->widget = twin_widget_create_with_handler(parent, background, width, - height, hstretch, vstretch, - custom_widget_dispatch); - if (!custom->widget) { - twin_free(custom->data); - twin_free(custom); + twin_custom_widget_t *custom = + twin_calloc(1, CUSTOM_DATA_OFFSET + data_size); + if (!custom) return NULL; - } - - register_custom_widget(custom->widget, custom, handler); - + twin_widget_layout_t preferred = { + .width = width, + .height = height, + .stretch_width = 1, + .stretch_height = 1, + }; + _twin_widget_init(&custom->widget, parent, 0, preferred, + _twin_custom_dispatch); + custom->widget.background = background; + custom->handler = handler; + custom->data = data_size ? (char *) custom + CUSTOM_DATA_OFFSET : NULL; return custom; } -void twin_custom_widget_destroy(twin_custom_widget_t *custom) -{ - if (!custom) - return; - - if (custom->widget) { - /* Unregister from dispatch map (idempotent if already done during - * TwinEventDestroy) - */ - unregister_custom_widget(custom->widget); - /* Note: widget destruction should be handled by the parent/container */ - } - twin_free(custom->data); - twin_free(custom); -} - -twin_pixmap_t *twin_widget_pixmap(twin_widget_t *widget) +static twin_pixmap_t *twin_widget_pixmap(twin_widget_t *widget) { if (!widget || !widget->window) return NULL; return widget->window->pixmap; } -twin_custom_widget_t *twin_widget_get_custom(twin_widget_t *widget) -{ - return find_custom_widget(widget); -} - void *twin_custom_widget_data(twin_custom_widget_t *custom) { return custom ? custom->data : NULL; } -twin_widget_t *twin_custom_widget_base(twin_custom_widget_t *custom) +twin_coord_t twin_custom_widget_width(twin_custom_widget_t *custom) { - return custom ? custom->widget : NULL; + return custom ? twin_widget_width(&custom->widget) : 0; } -twin_fixed_t twin_custom_widget_width(twin_custom_widget_t *custom) +twin_coord_t twin_custom_widget_height(twin_custom_widget_t *custom) { - return custom ? twin_widget_width(custom->widget) : 0; -} - -twin_fixed_t twin_custom_widget_height(twin_custom_widget_t *custom) -{ - return custom ? twin_widget_height(custom->widget) : 0; + return custom ? twin_widget_height(&custom->widget) : 0; } void twin_custom_widget_queue_paint(twin_custom_widget_t *custom) { - if (custom && custom->widget) - twin_widget_queue_paint(custom->widget); + if (custom) + twin_widget_queue_paint(&custom->widget); } twin_pixmap_t *twin_custom_widget_pixmap(twin_custom_widget_t *custom) { - return custom ? twin_widget_pixmap(custom->widget) : NULL; + return custom ? twin_widget_pixmap(&custom->widget) : NULL; } -void twin_widget_set_callback(twin_widget_t *widget, +bool twin_widget_set_callback(twin_widget_t *widget, twin_widget_proc_t callback, void *data) { if (!widget) - return; + return false; twin_widget_ext_t *ext = _twin_widget_ensure_ext(widget); if (!ext) - return; + return false; ext->callback = callback; ext->callback_data = data; -} - -void twin_widget_set_focusable(twin_widget_t *widget, bool focusable) -{ - if (!widget) - return; - - if (!focusable) { - if (widget->ext) - widget->ext->want_focus = false; - if (widget->parent && widget->parent->focus == widget) - widget->parent->focus = NULL; - return; - } - - twin_widget_ext_t *ext = _twin_widget_ensure_ext(widget); - if (!ext) - return; - ext->want_focus = true; + return true; } diff --git a/src/window.c b/src/window.c index 69c9bac3..6079ce1f 100644 --- a/src/window.c +++ b/src/window.c @@ -1,30 +1,110 @@ /* * Twin - A Tiny Window System * Copyright (c) 2004 Keith Packard - * Copyright (c) 2024-2025 National Cheng Kung University, Taiwan + * Copyright (c) 2024-2026 National Cheng Kung University, Taiwan * All rights reserved. */ +/* + * Window core + * + * A window is a pixmap plus client callbacks. Geometry, decorations, + * stacking and pointer policy belong to the active window manager + * (screen->wm); this file only keeps the bookkeeping consistent: + * + * - screen->windows lists every window bottom-to-top. Policies treat that + * order as the stacking order and raise a window by moving it to the end. + * - window->request is what the application asked for; window->client is + * where the policy put the client area inside the pixmap. + * - A window whose pixels are not needed (hidden under a full-screen app) + * can be evicted: its pixmap becomes a storage-less stub and is redrawn + * from client state when it is shown again. + */ + #include +#include #include "twin_private.h" -#if defined(CONFIG_WINDOW_MANAGER) +static void _twin_window_link(twin_window_t *window) +{ + twin_window_t **pp = &window->screen->windows; + while (*pp) + pp = &(*pp)->wm_next; + window->wm_next = NULL; + *pp = window; +} + +static void _twin_window_unlink(twin_window_t *window) +{ + for (twin_window_t **pp = &window->screen->windows; *pp; + pp = &(*pp)->wm_next) { + if (*pp == window) { + /* window->wm_next stays: a walk standing on a window that is + * destroyed under it can still step to the rest of the list */ + *pp = window->wm_next; + return; + } + } +} + +#if TWIN_WM_BARS +bool _twin_window_is_bar(const twin_window_t *window) +{ + return window == window->screen->bar_top || + window == window->screen->bar_bottom; +} +#endif + +bool _twin_window_shown(const twin_window_t *window) +{ + return window->pixmap && window->pixmap->screen && + !_twin_pixmap_is_stub(window->pixmap); +} -#define TWIN_ACTIVE_BG 0xd03b80ae -#define TWIN_INACTIVE_BG 0xffb0b0b0 -#define TWIN_FRAME_TEXT 0xffffffff -#define TWIN_ACTIVE_BORDER 0xff606060 -#define TWIN_INACTIVE_BORDER 0xff909090 -#define TWIN_BW 0 -#define TWIN_TITLE_HEIGHT 20 -#define TWIN_RESIZE_SIZE ((TWIN_TITLE_HEIGHT + 4) / 5) -#define TWIN_TITLE_BW ((TWIN_TITLE_HEIGHT + 11) / 12) +bool _twin_window_managed(const twin_window_t *window) +{ + return window->mapped && window->style != TwinWindowPlain && + !_twin_window_is_bar(window); +} -/* Shadow color for CSS-style effect: semi-transparent black (50% opacity). */ -#define SHADOW_COLOR 0x80000000 +/* Keep the compositor's notion of opaque pixels in sync with the window */ +static void _twin_window_update_opaque(twin_window_t *window) +{ + twin_pixmap_t *pixmap = window->pixmap; + if (!pixmap || _twin_pixmap_is_stub(pixmap)) + return; + if (pixmap->format == TWIN_RGB16) + pixmap->opaque_rect = + (twin_rect_t) {0, pixmap->width, 0, pixmap->height}; + else if (window->opaque) + pixmap->opaque_rect = window->client; + else + pixmap->opaque_rect = (twin_rect_t) {0, 0, 0, 0}; +} -#endif /* CONFIG_WINDOW_MANAGER */ +/* Clip to @r (pixmap coordinates) and put the origin at the client corner */ +static void _twin_window_clip(twin_window_t *window, const twin_rect_t *r) +{ + twin_pixmap_t *pixmap = window->pixmap; + twin_pixmap_reset_clip(pixmap); + pixmap->origin_x = pixmap->origin_y = 0; + twin_pixmap_clip(pixmap, r->left, r->top, r->right, r->bottom); + pixmap->origin_x = window->client.left; + pixmap->origin_y = window->client.top; +} + +static void _twin_window_reset_clip(twin_window_t *window) +{ + _twin_window_clip(window, &window->client); +} + +static void _twin_window_damage_all(twin_window_t *window) +{ + window->damage[0] = window->client; + window->n_damage = 1; + window->frame_dirty = true; +} twin_window_t *twin_window_create(twin_screen_t *screen, twin_format_t format, @@ -34,256 +114,296 @@ twin_window_t *twin_window_create(twin_screen_t *screen, twin_coord_t width, twin_coord_t height) { - twin_window_t *window = twin_malloc(sizeof(twin_window_t)); - twin_coord_t left, top, right, bottom; + twin_window_t *window = twin_calloc(1, sizeof(twin_window_t)); + twin_rect_t geom, border; if (!window) return NULL; window->screen = screen; window->style = style; - window->active = false; - window->iconify = false; - -#if defined(CONFIG_WINDOW_MANAGER) - switch (window->style) { - case TwinWindowApplication: - left = TWIN_BW; - top = TWIN_BW + TWIN_TITLE_HEIGHT + TWIN_BW; - right = TWIN_BW; - bottom = TWIN_BW; - break; - case TwinWindowPlain: - default: - left = 0; - top = 0; - right = 0; - bottom = 0; - break; - } - width += left + right; - height += top + bottom; -#else - /* No-WM: the single window always covers the entire screen - * regardless of what the application requested. The screen - * dimensions are authoritative -- they come from the backend - * (hardware resolution for fbdev, window size for SDL, etc.). - */ - left = 0; - top = 0; - right = 0; - bottom = 0; - x = 0; - y = 0; - width = screen->width; - height = screen->height; -#endif + window->format = format; + window->request = (twin_rect_t) {x, x + width, y, y + height}; + window->opaque = format == TWIN_RGB16; - window->client.left = left; - window->client.top = top; - window->client.right = width - right; - window->client.bottom = height - bottom; -#if defined(CONFIG_DROP_SHADOW) - /* Handle drop shadow. - * - * Allocate extra space in the pixmap for the drop shadow. The dimensions - * match the mask footprint generated by twin_shadow_border(): the shadow - * offset plus the blur-shaped fade-out region on the right and bottom - * edges. - */ - window->shadow_x = TWIN_SHADOW_EXTENT_X; - window->shadow_y = TWIN_SHADOW_EXTENT_Y; - window->pixmap = twin_pixmap_create(format, width + window->shadow_x, - height + window->shadow_y); -#else - window->shadow_x = 0; - window->shadow_y = 0; - window->pixmap = twin_pixmap_create(format, width, height); -#endif + _twin_wm_layout(window, &geom, &border); + window->pixmap = twin_pixmap_create(format, geom.right - geom.left, + geom.bottom - geom.top); if (!window->pixmap) { twin_free(window); return NULL; } - twin_pixmap_clip(window->pixmap, window->client.left, window->client.top, - window->client.right, window->client.bottom); - twin_pixmap_origin_to_clip(window->pixmap); window->pixmap->window = window; - twin_pixmap_move(window->pixmap, x, y); - window->damage = window->client; - window->client_grab = false; - window->draw_queued = false; - window->client_data = 0; - window->name = 0; - - window->draw = 0; - window->event = 0; - window->destroy = 0; + window->pixmap->x = geom.left; + window->pixmap->y = geom.top; + window->client = (twin_rect_t) { + border.left, + window->pixmap->width - border.right, + border.top, + window->pixmap->height - border.bottom, + }; + _twin_window_reset_clip(window); + _twin_window_update_opaque(window); + _twin_window_damage_all(window); + _twin_window_link(window); return window; } void twin_window_destroy(twin_window_t *window) { - twin_window_hide(window); + twin_screen_t *screen = window->screen; + + /* Stop queued repaints from touching freed memory. Clearing the work + * item matters: a later object at the same address would otherwise + * look like a live owner to the closure tracker. */ + twin_clear_work(window->paint_work); + window->paint_work = NULL; + /* ...and refuse new ones from the policy or the destroy callback */ + window->draw_queued = true; + _twin_closure_mark_for_free(window); + _twin_closure_unregister(window); + + /* A destroyed bar unregisters itself; its rows go back to the work + * area once it is gone (below) */ + bool was_bar = _twin_window_is_bar(window); +#if TWIN_WM_BARS + if (screen->bar_top == window) + screen->bar_top = NULL; + if (screen->bar_bottom == window) + screen->bar_bottom = NULL; +#endif + + /* Always tell the policy, even for unmapped windows, so it can drop any + * reference it still holds (e.g. a window taking part in a transition). + * A closing screen's policy has let go of every window already. */ + window->mapped = false; + if (screen->closing) + twin_pixmap_hide(window->pixmap); + else + _twin_wm_unmap(window, true); /* Call the destroy callback if set to clean up window contents */ if (window->destroy) (*window->destroy)(window); - twin_pixmap_destroy(window->pixmap); + /* The callback may have scheduled work or timeouts on the window; the + * unregister above dropped the mark, so drop the entry again now that + * nothing legitimate can use it. */ + twin_clear_work(window->paint_work); + window->paint_work = NULL; + _twin_closure_unregister(window); + + _twin_window_unlink(window); + + /* The policy may have adopted the pixmap to animate the departure. */ + if (window->pixmap) { + _twin_screen_forget(screen, window->pixmap); + twin_pixmap_destroy(window->pixmap); + } +#if defined(CONFIG_WM_DESKTOP) twin_free(window->name); - twin_free(window); + window->name = NULL; +#endif + window->pixmap = NULL; + window->event = NULL; + window->draw = NULL; + window->destroy = NULL; + window->dead = true; + + if (was_bar && !screen->closing) + _twin_screen_relayout(screen); + /* Whoever is running callbacks may still hold the pointer */ + if (screen->dispatching) { + window->zombie_next = screen->zombies; + screen->zombies = window; + } else { + twin_free(window); + } } void twin_window_show(twin_window_t *window) { - if (window->pixmap != window->screen->top) - twin_pixmap_show(window->pixmap, window->screen, window->screen->top); + window->mapped = true; + /* Nothing is shown any more on a closing screen */ + if (!window->screen->closing) + _twin_wm_map(window); } void twin_window_hide(twin_window_t *window) { - twin_pixmap_hide(window->pixmap); + if (!window->mapped) + return; + window->mapped = false; + if (window->screen->closing) + twin_pixmap_hide(window->pixmap); + else + _twin_wm_unmap(window, false); } -void twin_window_configure(twin_window_t *window, - twin_window_style_t style, - twin_coord_t x, - twin_coord_t y, - twin_coord_t width, - twin_coord_t height) +/* + * Swap window->pixmap for @fresh (already positioned) and free the old one. + * The compositing attributes, the stacking position and pending update + * disables move across. With @keep_focus the screen's active pixmap and + * pointer target follow the window; without it they are dropped, as when + * the surface is evicted. + */ +static void _twin_window_replace_pixmap(twin_window_t *window, + twin_pixmap_t *fresh, + bool keep_focus) { - bool need_repaint = false; - bool need_layout = false; - twin_rect_t border; + twin_pixmap_t *old = window->pixmap; + twin_screen_t *screen = window->screen; -#if defined(CONFIG_WINDOW_MANAGER) - _twin_window_style_size(style, &border); -#else - /* No-WM: always match the screen provided by the backend. */ - border.left = border.right = border.top = border.bottom = 0; - x = 0; - y = 0; - width = window->screen->width; - height = window->screen->height; + fresh->window = window; + fresh->opacity = old->opacity; +#if defined(CONFIG_DROP_SHADOW) + fresh->shadow = old->shadow; #endif - - twin_pixmap_disable_update(window->pixmap); - if (width != window->pixmap->width || height != window->pixmap->height) { - twin_pixmap_t *old = window->pixmap; - twin_coord_t pix_w = width + window->shadow_x; - twin_coord_t pix_h = height + window->shadow_y; - twin_pixmap_t *new_pixmap = - twin_pixmap_create(old->format, pix_w, pix_h); - if (!new_pixmap) { - twin_pixmap_enable_update(window->pixmap); - return; - } - int i; - - window->pixmap = new_pixmap; - window->pixmap->window = window; - twin_pixmap_move(window->pixmap, x, y); - if (old->screen) - twin_pixmap_show(window->pixmap, window->screen, old); - for (i = 0; i < old->disable; i++) - twin_pixmap_disable_update(window->pixmap); - twin_pixmap_destroy(old); - need_layout = true; - need_repaint = true; - } - if (style != window->style) { - need_layout = true; - window->style = style; - need_repaint = true; - } - if (need_layout) { - window->client.left = border.left; - window->client.top = border.top; - window->client.right = width - border.right; - window->client.bottom = height - border.bottom; - - twin_pixmap_reset_clip(window->pixmap); - twin_pixmap_clip(window->pixmap, window->client.left, - window->client.top, window->client.right, - window->client.bottom); - twin_pixmap_origin_to_clip(window->pixmap); + if (old->scan_height < old->height) + fresh->scan_height = min(old->scan_height, fresh->height); + if (old->screen) + twin_pixmap_show(fresh, old->screen, old); + for (int i = 0; i < old->disable; i++) + twin_pixmap_disable_update(fresh); + if (keep_focus) { + if (screen->active == old) + screen->active = fresh; + if (screen->target == old) + screen->target = fresh; + } else { + _twin_screen_forget(screen, old); } - if (x != window->pixmap->x || y != window->pixmap->y) - twin_pixmap_move(window->pixmap, x, y); - if (need_repaint) - twin_window_draw(window); - twin_pixmap_enable_update(window->pixmap); + twin_pixmap_destroy(old); + window->pixmap = fresh; } -#if defined(CONFIG_WINDOW_MANAGER) - -bool _twin_window_valid_range(twin_window_t *window, - twin_coord_t x, - twin_coord_t y) +void _twin_window_relayout(twin_window_t *window) { - twin_coord_t offset_x = window->shadow_x; - twin_coord_t offset_y = window->shadow_y; + twin_rect_t geom, border; + twin_pixmap_t *pixmap = window->pixmap; - switch (window->style) { - case TwinWindowPlain: - default: - if (window->pixmap->x <= x && - x < window->pixmap->x + window->pixmap->width - offset_x && - window->pixmap->y <= y && - y < window->pixmap->y + window->pixmap->height - offset_y) - return true; - return false; - case TwinWindowApplication: - if (window->pixmap->x <= x && - x < window->pixmap->x + window->pixmap->width - offset_x && - window->pixmap->y <= y && - y < window->pixmap->y + window->pixmap->height - offset_y) { - if (y < window->pixmap->y + (window->client.top)) - return !twin_pixmap_transparent(window->pixmap, x, y); - return !window->iconify; - } - return false; + /* A window being destroyed may have handed its pixmap to the policy; + * a closing screen lays nothing out */ + if (!pixmap || window->screen->closing) + return; + _twin_wm_layout(window, &geom, &border); + twin_coord_t width = geom.right - geom.left; + twin_coord_t height = geom.bottom - geom.top; + twin_rect_t old_client = window->client; + twin_rect_t client = {border.left, width - border.right, border.top, + height - border.bottom}; + + /* Same place, same size, same decorations: nothing to repaint */ + if (width == pixmap->width && height == pixmap->height && + window->format == pixmap->format && geom.left == pixmap->x && + geom.top == pixmap->y && !memcmp(&client, &old_client, sizeof(client))) + return; + + if (width != pixmap->width || height != pixmap->height || + window->format != pixmap->format) { + twin_pixmap_t *fresh = + _twin_pixmap_is_stub(pixmap) + ? _twin_pixmap_create_stub(window->format, width, height) + : twin_pixmap_create(window->format, width, height); + if (!fresh) + return; + fresh->x = geom.left; + fresh->y = geom.top; + _twin_window_replace_pixmap(window, fresh, true); + pixmap = fresh; } -} -void _twin_window_style_size(twin_window_style_t style, twin_rect_t *size) -{ - switch (style) { - case TwinWindowPlain: - default: - size->left = size->right = size->top = size->bottom = 0; - break; - case TwinWindowApplication: - size->left = TWIN_BW; - size->right = TWIN_BW; - size->top = TWIN_BW + TWIN_TITLE_HEIGHT + TWIN_BW; - size->bottom = TWIN_BW; - break; + window->client = client; + _twin_window_reset_clip(window); + _twin_window_update_opaque(window); + twin_pixmap_move(pixmap, geom.left, geom.top); + _twin_window_damage_all(window); + + /* Tell the client its size changed, then repaint everything. */ + if (window->event && (old_client.right - old_client.left != + window->client.right - window->client.left || + old_client.bottom - old_client.top != + window->client.bottom - window->client.top)) { + twin_event_t ev = {.kind = TwinEventConfigure}; + ev.u.configure.extents = + (twin_rect_t) {0, window->client.right - window->client.left, 0, + window->client.bottom - window->client.top}; + (*window->event)(window, &ev); + if (window->dead) + return; } + twin_window_draw(window); } -#else /* !CONFIG_WINDOW_MANAGER */ - -bool _twin_window_valid_range(twin_window_t *window, - twin_coord_t x, - twin_coord_t y) +void twin_window_configure(twin_window_t *window, + twin_window_style_t style, + twin_coord_t x, + twin_coord_t y, + twin_coord_t width, + twin_coord_t height) { - /* Simple bounds check without frame or shadow logic */ - return window->pixmap->x <= x && - x < window->pixmap->x + window->pixmap->width && - window->pixmap->y <= y && - y < window->pixmap->y + window->pixmap->height; + twin_screen_t *screen = window->screen; + twin_rect_t area = twin_screen_work_area(screen); + window->style = style; + window->request = (twin_rect_t) {x, x + width, y, y + height}; +#if defined(CONFIG_WM_DESKTOP) + window->wm_maximized = false; +#endif + /* The Configure handler may destroy the window: keep it readable */ + _twin_screen_busy(screen); + _twin_window_relayout(window); + /* A bar's height is part of everyone's work area */ + twin_rect_t now = twin_screen_work_area(screen); + if (memcmp(&area, &now, sizeof(area))) + _twin_screen_relayout(screen); + else if (window->mapped && !screen->closing) + /* The style may move the window between policy layers */ + _twin_wm_refresh(screen); + _twin_screen_idle(screen); } -void _twin_window_style_size(twin_window_style_t style, twin_rect_t *size) +void _twin_window_evict(twin_window_t *window) { - (void) style; - size->left = size->right = size->top = size->bottom = 0; + twin_pixmap_t *pixmap = window->pixmap; + if (!pixmap || _twin_pixmap_is_stub(pixmap)) + return; + /* Without a draw callback the pixels cannot be reconstructed */ + if (!window->draw) + return; + twin_pixmap_t *stub = + _twin_pixmap_create_stub(pixmap->format, pixmap->width, pixmap->height); + if (!stub) + return; + stub->x = pixmap->x; + stub->y = pixmap->y; + _twin_window_replace_pixmap(window, stub, false); + _twin_window_damage_all(window); } -#endif /* CONFIG_WINDOW_MANAGER */ +bool _twin_window_restore(twin_window_t *window) +{ + twin_pixmap_t *stub = window->pixmap; + if (!_twin_pixmap_is_stub(stub)) + return true; + twin_pixmap_t *pixmap = + twin_pixmap_create(stub->format, stub->width, stub->height); + if (!pixmap) + return false; + pixmap->x = stub->x; + pixmap->y = stub->y; + _twin_window_replace_pixmap(window, pixmap, true); + _twin_window_reset_clip(window); + _twin_window_update_opaque(window); + _twin_window_damage_all(window); + /* Painted before the next redisplay; a window covered and evicted + * again first (closing a stack of apps) is never painted at all */ + twin_window_queue_paint(window); + return true; +} void twin_window_set_name(twin_window_t *window, const char *name) { +#if defined(CONFIG_WM_DESKTOP) + /* Only title bars show it */ if (!name) return; char *new_name = twin_malloc(strlen(name) + 1); @@ -292,243 +412,60 @@ void twin_window_set_name(twin_window_t *window, const char *name) strcpy(new_name, name); twin_free(window->name); window->name = new_name; - twin_window_draw(window); + window->frame_dirty = true; + twin_window_queue_paint(window); +#else + (void) window; + (void) name; +#endif } -#if defined(CONFIG_WINDOW_MANAGER) - -static void twin_window_frame(twin_window_t *window) +void twin_window_set_opaque(twin_window_t *window, bool opaque) { - twin_fixed_t bw = twin_int_to_fixed(TWIN_TITLE_BW); - twin_path_t *path; - twin_fixed_t bw_2 = bw / 2; - twin_pixmap_t *pixmap = window->pixmap; - twin_fixed_t w_top = bw_2; - twin_fixed_t c_left = bw_2; - twin_fixed_t t_h = twin_int_to_fixed(window->client.top) - bw; - twin_fixed_t t_arc_1 = t_h / 3; - twin_fixed_t t_arc_2 = t_h * 2 / 3; - twin_fixed_t c_right = twin_int_to_fixed(window->client.right) - bw_2; - twin_fixed_t c_top = twin_int_to_fixed(window->client.top) - bw_2; - - twin_fixed_t name_height = t_h - bw - bw_2; - twin_fixed_t icon_size = name_height * 8 / 10; - twin_fixed_t icon_y = - (twin_int_to_fixed(window->client.top) - icon_size) / 2; - twin_fixed_t menu_x = t_arc_2; - twin_fixed_t text_x = menu_x + icon_size + bw; - twin_fixed_t text_y = icon_y + icon_size; - twin_fixed_t text_width; - twin_fixed_t title_right; - twin_fixed_t close_x; - twin_fixed_t restore_x; - twin_fixed_t iconify_x; - twin_fixed_t resize_x; - twin_fixed_t resize_y; - const char *name; - - twin_pixmap_reset_clip(pixmap); - twin_pixmap_origin_to_clip(pixmap); - - twin_fill(pixmap, 0x00000000, TWIN_SOURCE, 0, 0, pixmap->width, - window->client.top); - - path = twin_path_create(); - - - /* name */ - name = window->name; - if (!name) - name = "twin"; - twin_path_set_font_size(path, name_height); - twin_path_set_font_style(path, TwinStyleOblique | TwinStyleUnhinted); - text_width = twin_width_utf8(path, name); - - title_right = (text_x + text_width + bw + icon_size + bw + icon_size + bw + - icon_size + t_arc_2); - - if (title_right < c_right) - c_right = title_right; - - - close_x = c_right - t_arc_2 - icon_size; - restore_x = close_x - bw - icon_size; - iconify_x = restore_x - bw - icon_size; - resize_x = twin_int_to_fixed(window->client.right); - resize_y = twin_int_to_fixed(window->client.bottom); - - /* border */ - - twin_path_move(path, c_left, c_top); - twin_path_draw(path, c_right, c_top); - twin_path_curve(path, c_right, w_top + t_arc_1, c_right - t_arc_1, w_top, - c_right - t_h, w_top); - twin_path_draw(path, c_left + t_h, w_top); - twin_path_curve(path, c_left + t_arc_1, w_top, c_left, w_top + t_arc_1, - c_left, c_top); - twin_path_close(path); - - if (window->active) { - twin_paint_path(pixmap, TWIN_ACTIVE_BG, path); - twin_paint_stroke(pixmap, TWIN_ACTIVE_BORDER, path, bw_2 * 2); - } else { - twin_paint_path(pixmap, TWIN_INACTIVE_BG, path); - twin_paint_stroke(pixmap, TWIN_INACTIVE_BORDER, path, bw_2 * 2); - } - - twin_path_empty(path); - - twin_pixmap_clip(pixmap, twin_fixed_to_int(twin_fixed_floor(menu_x)), 0, - twin_fixed_to_int(twin_fixed_ceil(c_right - t_arc_2)), - window->client.top); - twin_pixmap_origin_to_clip(pixmap); - - twin_path_move(path, text_x - twin_fixed_floor(menu_x), text_y); - twin_path_utf8(path, name); - twin_paint_path(pixmap, TWIN_FRAME_TEXT, path); - - twin_pixmap_reset_clip(pixmap); - twin_pixmap_origin_to_clip(pixmap); - - /* widgets */ - - { - twin_matrix_t m; - - twin_matrix_identity(&m); - twin_matrix_translate(&m, menu_x, icon_y); - twin_matrix_scale(&m, icon_size, icon_size); - twin_icon_draw(pixmap, TwinIconMenu, m); - - twin_matrix_identity(&m); - twin_matrix_translate(&m, iconify_x, icon_y); - twin_matrix_scale(&m, icon_size, icon_size); - twin_icon_draw(pixmap, TwinIconIconify, m); - - twin_matrix_identity(&m); - twin_matrix_translate(&m, restore_x, icon_y); - twin_matrix_scale(&m, icon_size, icon_size); - twin_icon_draw(pixmap, TwinIconRestore, m); - - twin_matrix_identity(&m); - twin_matrix_translate(&m, close_x, icon_y); - twin_matrix_scale(&m, icon_size, icon_size); - twin_icon_draw(pixmap, TwinIconClose, m); - - twin_matrix_identity(&m); - twin_matrix_translate(&m, resize_x, resize_y); - twin_matrix_scale(&m, twin_int_to_fixed(TWIN_TITLE_HEIGHT), - twin_int_to_fixed(TWIN_TITLE_HEIGHT)); - twin_icon_draw(pixmap, TwinIconResize, m); - } - - twin_pixmap_clip(pixmap, window->client.left, window->client.top, - window->client.right, window->client.bottom); - twin_pixmap_origin_to_clip(pixmap); - - twin_path_destroy(path); -} - -#if defined(CONFIG_DROP_SHADOW) -static void twin_window_drop_shadow(twin_window_t *window) -{ - twin_pixmap_t *prev_active_pix = window->screen->top, - *active_pix = window->pixmap; - twin_source_u src; - twin_coord_t y, shadow_left, shadow_top; - - /* Remove the drop shadow from the previously active pixel map. */ - if (prev_active_pix) { - src.c = 0x00000000; - for (y = 0; y < prev_active_pix->height; y++) { - if (y < prev_active_pix->height - prev_active_pix->window->shadow_y) - twin_cover( - prev_active_pix, src.c, - prev_active_pix->width - prev_active_pix->window->shadow_x, - y, prev_active_pix->window->shadow_x); - else - twin_cover(prev_active_pix, src.c, 0, y, - prev_active_pix->width); - } - prev_active_pix->shadow = false; - } - - /* Mark the previously active pixel map as damaged to update its changes. */ - if (prev_active_pix && active_pix != prev_active_pix) - twin_pixmap_damage(prev_active_pix, 0, 0, prev_active_pix->width, - prev_active_pix->height); - - /* - * The shadow effect of the window only becomes visible when the window is - * active. - */ - active_pix->shadow = true; - /* - * Create a darker border of the active window that gives a more - * dimensional appearance. - */ - /* The shift offset and color of the shadow can be selected by the user. */ - twin_shadow_border(active_pix, SHADOW_COLOR); - - if (CONFIG_SHADOW_BLUR <= 0) - return; - - shadow_left = active_pix->width - active_pix->window->shadow_x; - shadow_top = active_pix->height - active_pix->window->shadow_y; - - /* Right side of the active window. */ - twin_stack_blur(active_pix, CONFIG_SHADOW_BLUR, shadow_left, - shadow_left + active_pix->window->shadow_x, 0, - shadow_top + active_pix->window->shadow_y); - /* Bottom side of the active window. */ - twin_stack_blur(active_pix, CONFIG_SHADOW_BLUR, 0, - shadow_left + active_pix->window->shadow_x, shadow_top, - shadow_top + active_pix->window->shadow_y); + window->opaque = opaque || window->format == TWIN_RGB16; + _twin_window_update_opaque(window); + /* Opacity decides what an opaque full-screen window covers */ + if (window->mapped && !window->screen->closing) + _twin_wm_refresh(window->screen); } -#endif /* CONFIG_DROP_SHADOW */ - -#endif /* CONFIG_WINDOW_MANAGER */ void twin_window_draw(twin_window_t *window) { twin_pixmap_t *pixmap = window->pixmap; -#if defined(CONFIG_WINDOW_MANAGER) - switch (window->style) { - case TwinWindowPlain: - default: - break; - case TwinWindowApplication: - twin_window_frame(window); - break; + /* Evicted: keep the damage, the restore will repaint everything. */ + if (_twin_pixmap_is_stub(pixmap)) + return; + + if (window->frame_dirty) { + window->frame_dirty = false; + if (TWIN_WM_DECORATES) { + _twin_wm_decorate(window); + _twin_window_reset_clip(window); + } } -#endif /* if no draw function or no damage, return */ - if (window->draw == NULL || (window->damage.left >= window->damage.right || - window->damage.top >= window->damage.bottom)) + if (window->draw == NULL || !window->n_damage) return; - /* clip to damaged area and draw */ - twin_pixmap_reset_clip(pixmap); - twin_pixmap_clip(pixmap, window->damage.left, window->damage.top, - window->damage.right, window->damage.bottom); - twin_screen_disable_update(window->screen); - - (*window->draw)(window); - - /* damage matching screen area */ - twin_pixmap_damage(pixmap, window->damage.left, window->damage.top, - window->damage.right, window->damage.bottom); + /* Paint each damaged rectangle under its own clip: two small updates + * at opposite ends of a window stay two small repaints. */ + twin_rect_t rects[TWIN_WINDOW_DAMAGE_RECTS]; + int n = window->n_damage; + memcpy(rects, window->damage, sizeof(rects[0]) * n); + window->n_damage = 0; + twin_screen_disable_update(window->screen); + for (int i = 0; i < n; i++) { + _twin_window_clip(window, &rects[i]); + (*window->draw)(window); + twin_pixmap_damage(pixmap, rects[i].left, rects[i].top, rects[i].right, + rects[i].bottom); + } twin_screen_enable_update(window->screen); - /* clear damage and restore clip */ - window->damage.left = window->damage.right = 0; - window->damage.top = window->damage.bottom = 0; - twin_pixmap_reset_clip(pixmap); - twin_pixmap_clip(pixmap, window->client.left, window->client.top, - window->client.right, window->client.bottom); + _twin_window_reset_clip(window); } /* window keep track of local damage */ @@ -546,22 +483,13 @@ void twin_window_damage(twin_window_t *window, right = window->client.right; if (bottom > window->client.bottom) bottom = window->client.bottom; + if (left >= right || top >= bottom) + return; - if (window->damage.left == window->damage.right) { - window->damage.left = left; - window->damage.right = right; - window->damage.top = top; - window->damage.bottom = bottom; - } else { - if (left < window->damage.left) - window->damage.left = left; - if (top < window->damage.top) - window->damage.top = top; - if (window->damage.right < right) - window->damage.right = right; - if (window->damage.bottom < bottom) - window->damage.bottom = bottom; - } + _twin_rect_list_add(window->damage, &window->n_damage, + TWIN_WINDOW_DAMAGE_RECTS, + (twin_rect_t) {left, right, top, bottom}); + twin_window_queue_paint(window); } static bool _twin_window_repaint(void *closure) @@ -569,6 +497,7 @@ static bool _twin_window_repaint(void *closure) twin_window_t *window = closure; window->draw_queued = false; + window->paint_work = NULL; twin_window_draw(window); return false; @@ -578,192 +507,197 @@ void twin_window_queue_paint(twin_window_t *window) { if (!window->draw_queued) { window->draw_queued = true; - twin_set_work(_twin_window_repaint, TWIN_WORK_PAINT, window); + window->paint_work = + twin_set_work(_twin_window_repaint, TWIN_WORK_PAINT, window); + if (!window->paint_work) + window->draw_queued = false; } } -#if defined(CONFIG_WINDOW_MANAGER) - -bool _twin_window_dispatch(twin_window_t *window, twin_event_t *event) +void twin_window_close(twin_window_t *window) { - twin_event_t ev = *event; - bool delegate = true; - - twin_fixed_t bw = twin_int_to_fixed(TWIN_TITLE_BW); - twin_fixed_t t_h = twin_int_to_fixed(window->client.top) - bw; - twin_fixed_t t_arc_2 = t_h * 2 / 3; - twin_fixed_t c_right = twin_int_to_fixed(window->client.right) - bw / 2; - twin_fixed_t name_height = t_h - bw - bw / 2; - twin_fixed_t icon_size = name_height * 8 / 10; - twin_fixed_t menu_x = t_arc_2; - twin_fixed_t text_x = menu_x + icon_size + bw; - twin_fixed_t text_width; - twin_fixed_t title_right; - twin_path_t *path = twin_path_create(); - const char *name = window->name; - if (!name) - name = "twin"; - - text_width = twin_width_utf8(path, name); - twin_path_destroy(path); - title_right = (text_x + text_width + bw + icon_size + bw + icon_size + bw + - icon_size + t_arc_2); + twin_screen_t *screen = window->screen; + twin_event_t ev = {.kind = TwinEventClose}; + /* The handler may destroy the window itself: keep it readable */ + _twin_screen_busy(screen); + if (!_twin_window_deliver(window, &ev) && !window->dead) + twin_window_destroy(window); + _twin_screen_idle(screen); +} - if (title_right < c_right) - c_right = title_right; +void twin_window_scroll(twin_window_t *window, + twin_rect_t area, + twin_coord_t dx, + twin_coord_t dy) +{ + twin_pixmap_t *pixmap = window->pixmap; - twin_fixed_t close_x = c_right - t_arc_2 - icon_size; - twin_fixed_t restore_x = close_x - bw - icon_size; - twin_fixed_t iconify_x = restore_x - bw - icon_size; - int local_x, local_y; + /* Client coordinates to pixmap coordinates, clipped to the client */ + area.left = max((twin_coord_t) (area.left + window->client.left), + window->client.left); + area.right = min((twin_coord_t) (area.right + window->client.left), + window->client.right); + area.top = + max((twin_coord_t) (area.top + window->client.top), window->client.top); + area.bottom = min((twin_coord_t) (area.bottom + window->client.top), + window->client.bottom); + if (area.left >= area.right || area.top >= area.bottom || (!dx && !dy)) + return; - switch (ev.kind) { - case TwinEventActivate: - /* - * Pure focus transition -- no pointer data in the event union. - * Update frame visuals but do not touch pointer coordinates. - */ - if (window->iconify) - window->active = false; - else - window->active = true; - twin_window_frame(window); - if (window->screen->top && window->screen->top->window && - window != window->screen->top->window) { - window->screen->top->window->active = false; - twin_window_frame(window->screen->top->window); - } -#if defined(CONFIG_DROP_SHADOW) - twin_window_drop_shadow(window); -#endif - delegate = false; - break; - case TwinEventButtonDown: - local_y = ev.u.pointer.screen_y - window->pixmap->y; - if (local_y >= 0 && local_y <= TWIN_BW + TWIN_TITLE_HEIGHT + TWIN_BW) { - local_x = ev.u.pointer.screen_x - window->pixmap->x; - if (local_x > twin_fixed_to_int(iconify_x) && - local_x < twin_fixed_to_int(restore_x)) { - window->iconify = true; - twin_pixmap_damage(window->pixmap, 0, 0, window->pixmap->width, - window->pixmap->height); - } else if (local_x > twin_fixed_to_int(restore_x) && - local_x < twin_fixed_to_int(close_x)) { - window->iconify = false; - twin_pixmap_damage(window->pixmap, 0, 0, window->pixmap->width, - window->pixmap->height); - } - } - /* Activate the window on click */ - if (window->iconify) - window->active = false; - else - window->active = true; - twin_window_frame(window); - if (window->screen->top && window->screen->top->window && - window != window->screen->top->window) { - window->screen->top->window->active = false; - twin_window_frame(window->screen->top->window); - } -#if defined(CONFIG_DROP_SHADOW) - twin_window_drop_shadow(window); -#endif - if (window->client.left <= ev.u.pointer.x && - ev.u.pointer.x < window->client.right && - window->client.top <= ev.u.pointer.y && - ev.u.pointer.y < window->client.bottom) { - delegate = true; - window->client_grab = true; - ev.u.pointer.x -= window->client.left; - ev.u.pointer.y -= window->client.top; - } else - delegate = false; - break; - case TwinEventButtonUp: - if (window->client_grab) { - delegate = true; - window->client_grab = false; - ev.u.pointer.x -= window->client.left; - ev.u.pointer.y -= window->client.top; - } else - delegate = false; - break; - case TwinEventMotion: - if (window->client_grab || (window->client.left <= ev.u.pointer.x && - ev.u.pointer.x < window->client.right && - window->client.top <= ev.u.pointer.y && - ev.u.pointer.y < window->client.bottom)) { - delegate = true; - ev.u.pointer.x -= window->client.left; - ev.u.pointer.y -= window->client.top; - } else - delegate = false; - break; - default: - break; + /* Pending damage marks stale pixels. The blit carries them along, so + * the damage has to follow: repaint where they land. (Painting first + * would be wrong: callers update their state, e.g. the scroll offset, + * before scrolling.) */ + int pending = window->n_damage; + twin_rect_t moved[TWIN_WINDOW_DAMAGE_RECTS]; + for (int i = 0; i < pending; i++) { + twin_rect_t d = window->damage[i]; + d.left = max(d.left, area.left) + dx; + d.right = min(d.right, area.right) + dx; + d.top = max(d.top, area.top) + dy; + d.bottom = min(d.bottom, area.bottom) + dy; + moved[i] = d; + } + for (int i = 0; i < pending; i++) + twin_window_damage( + window, max(moved[i].left, area.left), max(moved[i].top, area.top), + min(moved[i].right, area.right), min(moved[i].bottom, area.bottom)); + + twin_coord_t w = area.right - area.left, h = area.bottom - area.top; + if (_twin_pixmap_is_stub(pixmap) || dx >= w || -dx >= w || dy >= h || + -dy >= h) { + twin_window_damage(window, area.left, area.top, area.right, + area.bottom); + twin_window_queue_paint(window); + return; } - if (!window->event) - delegate = false; - - if (delegate && (*window->event)(window, &ev)) - return true; - /* - * simple window management - */ - switch (event->kind) { - case TwinEventButtonDown: - twin_window_show(window); - window->screen->button_x = event->u.pointer.x; - window->screen->button_y = event->u.pointer.y; - return true; - case TwinEventButtonUp: - window->screen->button_x = -1; - window->screen->button_y = -1; - return true; - case TwinEventMotion: - if (window->screen->button_x >= 0) { - twin_coord_t x, y; - - x = event->u.pointer.screen_x - window->screen->button_x; - y = event->u.pointer.screen_y - window->screen->button_y; - twin_window_configure(window, window->style, x, y, - window->pixmap->width, - window->pixmap->height); - } - return true; - default: - break; + /* Destination rectangle: the part of @area still covered after moving */ + twin_rect_t d = { + .left = area.left + max(dx, (twin_coord_t) 0), + .right = area.right + min(dx, (twin_coord_t) 0), + .top = area.top + max(dy, (twin_coord_t) 0), + .bottom = area.bottom + min(dy, (twin_coord_t) 0), + }; + int bpp = twin_bytes_per_pixel(pixmap->format); + size_t row_bytes = (size_t) (d.right - d.left) * bpp; + if (dy > 0) { + for (twin_coord_t y = d.bottom - 1; y >= d.top; y--) + memmove(twin_pixmap_pointer(pixmap, d.left, y).v, + twin_pixmap_pointer(pixmap, d.left - dx, y - dy).v, + row_bytes); + } else { + for (twin_coord_t y = d.top; y < d.bottom; y++) + memmove(twin_pixmap_pointer(pixmap, d.left, y).v, + twin_pixmap_pointer(pixmap, d.left - dx, y - dy).v, + row_bytes); } - return false; + twin_pixmap_damage(pixmap, d.left, d.top, d.right, d.bottom); + + /* Exposed bands */ + if (dy > 0) + twin_window_damage(window, area.left, area.top, area.right, d.top); + else if (dy < 0) + twin_window_damage(window, area.left, d.bottom, area.right, + area.bottom); + if (dx > 0) + twin_window_damage(window, area.left, area.top, d.left, area.bottom); + else if (dx < 0) + twin_window_damage(window, d.right, area.top, area.right, area.bottom); + twin_window_queue_paint(window); } -#else /* !CONFIG_WINDOW_MANAGER */ +bool _twin_window_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y) +{ +#if TWIN_WM_DECORATES + return _twin_wm_hit(window, x, y); +#else + twin_pixmap_t *p = window->pixmap; + return p->x <= x && x < p->x + p->width && p->y <= y && + y < p->y + p->scan_height; +#endif +} -bool _twin_window_dispatch(twin_window_t *window, twin_event_t *event) +/* Forward an event to the client, in client coordinates */ +bool _twin_window_deliver(twin_window_t *window, twin_event_t *event) { twin_event_t ev = *event; - /* - * No-WM dispatch: adjust pointer coordinates by client offset (both - * zero in No-WM mode, so effectively a no-op) and delegate directly - * to window->event. No drag, no iconify, no click-to-raise. - */ switch (ev.kind) { case TwinEventButtonDown: case TwinEventButtonUp: case TwinEventMotion: + case TwinEventEnter: + case TwinEventLeave: ev.u.pointer.x -= window->client.left; ev.u.pointer.y -= window->client.top; break; default: break; } - if (window->event) return (*window->event)(window, &ev); - return false; } -#endif /* CONFIG_WINDOW_MANAGER */ +bool _twin_window_dispatch(twin_window_t *window, twin_event_t *event) +{ + return _twin_wm_dispatch(window, event); +} + +/* Move @window to the top of the stacking order */ +void _twin_window_raise(twin_window_t *window) +{ + _twin_window_unlink(window); + _twin_window_link(window); +} + +twin_window_t *twin_screen_first_window(const twin_screen_t *screen) +{ + return screen ? screen->windows : NULL; +} + +twin_window_t *twin_window_next(const twin_window_t *window) +{ + return window->wm_next; +} + +twin_pixmap_t *twin_window_pixmap(const twin_window_t *window) +{ + return window->pixmap; +} + +twin_rect_t twin_window_client(const twin_window_t *window) +{ + return window->client; +} + +twin_window_style_t twin_window_style(const twin_window_t *window) +{ + return window->style; +} + +bool twin_window_is_mapped(const twin_window_t *window) +{ + return window->mapped; +} + +void twin_window_set_callbacks(twin_window_t *window, + twin_draw_func_t draw, + twin_event_func_t event, + twin_destroy_func_t destroy) +{ + window->draw = draw; + window->event = event; + window->destroy = destroy; +} + +void twin_window_set_client_data(twin_window_t *window, void *data) +{ + window->client_data = data; +} + +void *twin_window_client_data(const twin_window_t *window) +{ + return window->client_data; +} diff --git a/src/wm-desktop.c b/src/wm-desktop.c new file mode 100644 index 00000000..347f1d49 --- /dev/null +++ b/src/wm-desktop.c @@ -0,0 +1,696 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2004 Keith Packard + * Copyright (c) 2024-2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Desktop window manager policy + * + * Overlapping windows with a vector-drawn title bar, click-to-raise, + * drag-to-move, iconify/restore, close, and a drop shadow under the + * active window. The shadow is a compositor attribute, so activating a + * window costs a damage rectangle rather than a blur pass. + * + * Also: double-click the title to maximize to the work area (and back), + * drop a dragged title on the left or right screen edge to snap to that + * half, or on the top edge to maximize; drag an unclaimed press in the + * bottom-right corner of the client to resize. Windows always keep part + * of the title bar on screen. + */ + +#include "twin_private.h" + +#define TWIN_ACTIVE_BG 0xd03b80ae +#define TWIN_INACTIVE_BG 0xffb0b0b0 +#define TWIN_FRAME_TEXT 0xffffffff +#define TWIN_ACTIVE_BORDER 0xff606060 +#define TWIN_INACTIVE_BORDER 0xff909090 +#define TWIN_BW 0 +#define TWIN_TITLE_HEIGHT 20 +#define TWIN_TITLE_BW ((TWIN_TITLE_HEIGHT + 11) / 12) + +#if defined(CONFIG_DROP_SHADOW) +#define SHADOW_ALPHA 0x80 +#endif + +#define DOUBLE_CLICK_MS 300 +#define RESIZE_GRIP 12 /* bottom-right corner of the client that resizes */ +#define MIN_CLIENT_W 48 /* smallest client a resize leaves */ +#define MIN_CLIENT_H 24 +#define TITLE_VISIBLE 32 /* title pixels a drag must leave on screen */ +#define SNAP_EDGE 2 /* pointer this close to an edge snaps on release */ + +typedef struct { + twin_window_t *drag; /* window being moved */ + twin_window_t *resize; /* window being resized */ + /* Drag: grab point inside the frame. Resize: pointer minus client + * size, so the size is the pointer minus this */ + twin_coord_t button_x, button_y; + twin_coord_t resize_w, resize_h; /* size to apply on the next frame */ + twin_work_t *resize_work; /* pending application, at most one */ + twin_window_t *click; /* last title click, for double-click */ + twin_time_t click_time; +} twin_desktop_t; + +#define DESKTOP(screen) ((twin_desktop_t *) _twin_wm_state(screen)) + +const size_t _twin_wm_state_size = sizeof(twin_desktop_t); + +void _twin_wm_layout(twin_window_t *window, + twin_rect_t *geom, + twin_rect_t *border) +{ + twin_screen_t *screen = window->screen; + const twin_rect_t *r = &window->request; + + *border = (twin_rect_t) {0, 0, 0, 0}; + switch (window->style) { + case TwinWindowFullscreen: + *geom = (twin_rect_t) {0, screen->width, 0, screen->height}; + return; + case TwinWindowApplication: + border->left = TWIN_BW; + border->right = TWIN_BW; + border->top = TWIN_BW + TWIN_TITLE_HEIGHT + TWIN_BW; + border->bottom = TWIN_BW; + break; + case TwinWindowPlain: + default: + break; + } + /* request holds the frame origin and the client size */ + geom->left = r->left; + geom->top = r->top; + geom->right = r->right + border->left + border->right; + geom->bottom = r->bottom + border->top + border->bottom; +} + +/* Title bar geometry shared by painting and hit testing */ +typedef struct { + twin_fixed_t bw, t_h, t_arc_1, t_arc_2; + twin_fixed_t c_left, c_right, c_top, w_top; + twin_fixed_t name_height, icon_size, icon_y; + twin_fixed_t menu_x, text_x, text_y; + twin_fixed_t close_x, restore_x, iconify_x; +} twin_title_t; + +static const char *_title_name(const twin_window_t *window) +{ + return window->name ? window->name : "twin"; +} + +static void _title_metrics(twin_window_t *window, twin_title_t *t) +{ + t->bw = twin_int_to_fixed(TWIN_TITLE_BW); + twin_fixed_t bw_2 = t->bw / 2; + t->w_top = bw_2; + t->c_left = bw_2; + t->t_h = twin_int_to_fixed(window->client.top) - t->bw; + t->t_arc_1 = t->t_h / 3; + t->t_arc_2 = t->t_h * 2 / 3; + t->c_right = twin_int_to_fixed(window->client.right) - bw_2; + t->c_top = twin_int_to_fixed(window->client.top) - bw_2; + t->name_height = t->t_h - t->bw - bw_2; + t->icon_size = t->name_height * 8 / 10; + t->icon_y = (twin_int_to_fixed(window->client.top) - t->icon_size) / 2; + t->menu_x = t->t_arc_2; + t->text_x = t->menu_x + t->icon_size + t->bw; + t->text_y = t->icon_y + t->icon_size; + + twin_path_t *path = _twin_path_acquire(window->screen); + twin_fixed_t text_width = 0; + if (path) { + twin_path_set_font_size(path, t->name_height); + twin_path_set_font_style(path, TwinStyleOblique | TwinStyleUnhinted); + text_width = twin_width_utf8(path, _title_name(window)); + _twin_path_release(window->screen, path); + } + twin_fixed_t title_right = t->text_x + text_width + t->bw + t->icon_size + + t->bw + t->icon_size + t->bw + t->icon_size + + t->t_arc_2; + if (title_right < t->c_right) + t->c_right = title_right; + t->close_x = t->c_right - t->t_arc_2 - t->icon_size; + t->restore_x = t->close_x - t->bw - t->icon_size; + t->iconify_x = t->restore_x - t->bw - t->icon_size; +} + +static void _draw_icon(twin_pixmap_t *pixmap, + twin_icon_t icon, + twin_fixed_t x, + twin_fixed_t y, + twin_fixed_t size) +{ + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_translate(&m, x, y); + twin_matrix_scale(&m, size, size); + twin_icon_draw(pixmap, icon, m); +} + +void _twin_wm_decorate(twin_window_t *window) +{ + twin_pixmap_t *pixmap = window->pixmap; + twin_title_t t; + + if (window->style != TwinWindowApplication) + return; + _title_metrics(window, &t); + + twin_pixmap_reset_clip(pixmap); + pixmap->origin_x = pixmap->origin_y = 0; + twin_fill(pixmap, 0x00000000, TWIN_SOURCE, 0, 0, pixmap->width, + window->client.top); + + twin_path_t *path = _twin_path_acquire(window->screen); + if (!path) + return; + + /* Tab-shaped title bar */ + twin_path_move(path, t.c_left, t.c_top); + twin_path_draw(path, t.c_right, t.c_top); + twin_path_curve(path, t.c_right, t.w_top + t.t_arc_1, t.c_right - t.t_arc_1, + t.w_top, t.c_right - t.t_h, t.w_top); + twin_path_draw(path, t.c_left + t.t_h, t.w_top); + twin_path_curve(path, t.c_left + t.t_arc_1, t.w_top, t.c_left, + t.w_top + t.t_arc_1, t.c_left, t.c_top); + twin_path_close(path); + twin_paint_path(pixmap, window->active ? TWIN_ACTIVE_BG : TWIN_INACTIVE_BG, + path); + twin_paint_stroke( + pixmap, window->active ? TWIN_ACTIVE_BORDER : TWIN_INACTIVE_BORDER, + path, t.bw); + twin_path_empty(path); + + /* Name, clipped between the menu icon and the buttons */ + twin_pixmap_clip(pixmap, twin_fixed_to_int(twin_fixed_floor(t.menu_x)), 0, + twin_fixed_to_int(twin_fixed_ceil(t.c_right - t.t_arc_2)), + window->client.top); + twin_pixmap_origin_to_clip(pixmap); + twin_path_set_font_size(path, t.name_height); + twin_path_set_font_style(path, TwinStyleOblique | TwinStyleUnhinted); + twin_path_move(path, t.text_x - twin_fixed_floor(t.menu_x), t.text_y); + twin_path_utf8(path, _title_name(window)); + twin_paint_path(pixmap, TWIN_FRAME_TEXT, path); + _twin_path_release(window->screen, path); + + twin_pixmap_reset_clip(pixmap); + pixmap->origin_x = pixmap->origin_y = 0; + _draw_icon(pixmap, TwinIconMenu, t.menu_x, t.icon_y, t.icon_size); + _draw_icon(pixmap, TwinIconIconify, t.iconify_x, t.icon_y, t.icon_size); + _draw_icon(pixmap, TwinIconRestore, t.restore_x, t.icon_y, t.icon_size); + _draw_icon(pixmap, TwinIconClose, t.close_x, t.icon_y, t.icon_size); + + twin_pixmap_damage(pixmap, 0, 0, pixmap->width, window->client.top); +} + +/* Iconified: only the title bar rows are shown (see scan_height) */ +static bool _iconified(const twin_window_t *window) +{ + return window->pixmap->scan_height < window->pixmap->height; +} + +static void _set_active(twin_window_t *window, bool active) +{ + if (window->active == 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); +#endif + if (window->style == TwinWindowApplication) { + window->frame_dirty = true; + twin_window_draw(window); + } +} + +/* Topmost window that can take the keyboard, other than @skip */ +static twin_window_t *_focus_candidate(twin_screen_t *screen, + const twin_window_t *skip) +{ + twin_window_t *best = NULL; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w != skip && _twin_window_managed(w) && _twin_window_shown(w) && + !_iconified(w)) + best = w; + } + return best; +} + +/* Give @window (NULL: nobody) the keyboard and deactivate every other */ +static void _focus(twin_screen_t *screen, twin_window_t *window) +{ + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w != window && w->pixmap) + _set_active(w, false); + } + if (window) + _set_active(window, true); + twin_pixmap_t *p = window ? window->pixmap : NULL; + if (screen->active != p) + twin_screen_set_active(screen, p); +} + +/* Activate @window; an iconified window shows no content, so the keyboard + * goes to the topmost window that does */ +static void _activate(twin_window_t *window) +{ + twin_screen_t *screen = window->screen; + _focus(screen, + _iconified(window) ? _focus_candidate(screen, window) : window); +} + +/* Give an application window a spot of its own the first time it shows: + * cascade off any earlier window at the same origin (screens built for a + * phone all ask for the same spot) and keep the frame on screen */ +static void _clamp_to_screen(twin_window_t *w); + +static void _offset(twin_rect_t *r, twin_coord_t dx, twin_coord_t dy) +{ + r->left += dx; + r->right += dx; + r->top += dy; + r->bottom += dy; +} + +static void _place_new(twin_window_t *w) +{ + twin_screen_t *screen = w->screen; + w->wm_placed = true; + if (w->style != TwinWindowApplication) + return; + twin_rect_t base = w->request; + twin_coord_t step = TWIN_TITLE_HEIGHT + 4; + for (int k = 0; k < 16; k++) { + w->request = base; + _offset(&w->request, k * step, k * step); + _clamp_to_screen(w); + bool clash = false; + for (twin_window_t *o = screen->windows; o; o = o->wm_next) + if (o != w && o->wm_placed && o->style == TwinWindowApplication && + o->request.left == w->request.left && + o->request.top == w->request.top) + clash = true; + if (!clash) + return; + } + w->request = base; + _clamp_to_screen(w); +} + +void _twin_wm_map(twin_window_t *window) +{ + twin_screen_t *screen = window->screen; + + if (_twin_window_is_bar(window)) + return; + if (!window->wm_placed) { + _place_new(window); + _twin_window_relayout(window); /* no-op unless it moved */ + } + _twin_window_restore(window); + _twin_window_raise(window); + if (window->pixmap != screen->top) + twin_pixmap_show(window->pixmap, screen, screen->top); + if (window->style != TwinWindowPlain) + _activate(window); +} + +/* twin_window_activate(): an explicit request also restores */ +void _twin_wm_activate(twin_window_t *window) +{ + _twin_pixmap_set_scan_height(window->pixmap, window->pixmap->height); + _twin_wm_map(window); +} + +void _twin_wm_unmap(twin_window_t *window, bool destroying) +{ + twin_screen_t *screen = window->screen; + twin_desktop_t *d = DESKTOP(screen); + (void) destroying; + + if (d->drag == window) + d->drag = NULL; + if (d->resize == window) + d->resize = NULL; + if (d->click == window) + d->click = NULL; + /* Only the window that had the keyboard hands it on */ + bool had_focus = + window->active || (screen->active && screen->active == window->pixmap); + _set_active(window, false); + twin_pixmap_hide(window->pixmap); + if (!had_focus) + return; + twin_window_t *next = _focus_candidate(screen, window); + if (next) + _focus(screen, next); +} + +/* Pixel test so clicks on the rounded title corners fall through */ +static bool _frame_transparent(twin_pixmap_t *p, twin_coord_t x, twin_coord_t y) +{ + if (_twin_pixmap_is_stub(p)) + return true; + twin_pointer_t q = twin_pixmap_pointer(p, x - p->x, y - p->y); + switch (p->format) { + case TWIN_A8: + return *q.a8 == 0; + case TWIN_ARGB32: + return (*q.argb32 >> 24) == 0; + default: + return false; + } +} + +bool _twin_wm_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y) +{ + twin_pixmap_t *p = window->pixmap; + if (x < p->x || x >= p->x + p->width || y < p->y || y >= p->y + p->height) + return false; + if (window->style != TwinWindowApplication) + return true; + if (y < p->y + window->client.top) + return !_frame_transparent(p, x, y); + return y < p->y + p->scan_height; +} + +/* Frame at (@x, @y) with a @cw x @ch client, re-laid out at once */ +static void _set_frame(twin_window_t *window, + twin_coord_t x, + twin_coord_t y, + twin_coord_t cw, + twin_coord_t ch) +{ + window->request = (twin_rect_t) {x, x + cw, y, y + ch}; + _twin_window_relayout(window); +} + +/* Fill @area (a work-area rectangle) with @window's frame */ +static void _fill_area(twin_window_t *window, twin_rect_t area) +{ + if (!window->wm_maximized) { + window->wm_saved = window->request; + window->wm_maximized = true; + } + _set_frame(window, area.left, area.top, area.right - area.left, + max((twin_coord_t) (area.bottom - area.top - TWIN_TITLE_HEIGHT), + (twin_coord_t) MIN_CLIENT_H)); +} + +static void _unmaximize(twin_window_t *window) +{ + if (!window->wm_maximized) + return; + window->wm_maximized = false; + twin_rect_t r = window->wm_saved; + _set_frame(window, r.left, r.top, r.right - r.left, r.bottom - r.top); +} + +static void _toggle_maximize(twin_window_t *window) +{ + if (window->wm_maximized) + _unmaximize(window); + else + _fill_area(window, twin_screen_work_area(window->screen)); +} + +/* Keep enough of the title bar on screen to grab it again */ +static void _clamp_title(const twin_screen_t *screen, + twin_coord_t frame_w, + twin_coord_t *x, + twin_coord_t *y) +{ + twin_coord_t keep = min((twin_coord_t) TITLE_VISIBLE, frame_w); + *x = max(*x, (twin_coord_t) (keep - frame_w)); + *x = min(*x, (twin_coord_t) (screen->width - keep)); + *y = max(*y, (twin_coord_t) 0); + *y = min(*y, (twin_coord_t) (screen->height - TWIN_TITLE_HEIGHT)); +} + +/* Released a title drag at screen (@x, @y): snap on the screen edges */ +static void _drop(twin_window_t *window, twin_coord_t x, twin_coord_t y) +{ + twin_screen_t *screen = window->screen; + twin_rect_t area = twin_screen_work_area(screen); + twin_coord_t half = (area.right - area.left) / 2; + if (y <= area.top + SNAP_EDGE) { + _fill_area(window, area); + } else if (x <= SNAP_EDGE) { + area.right = area.left + half; + _fill_area(window, area); + } else if (x >= screen->width - 1 - SNAP_EDGE) { + area.left = area.right - half; + _fill_area(window, area); + } +} + +static bool _in_grip(const twin_window_t *window, + twin_coord_t x, + twin_coord_t y) +{ + return window->style == TwinWindowApplication && + x >= window->client.right - RESIZE_GRIP && + y >= window->client.bottom - RESIZE_GRIP; +} + +static bool _title_press(twin_window_t *window, twin_event_t *event) +{ + twin_screen_t *screen = window->screen; + twin_desktop_t *d = DESKTOP(screen); + twin_pixmap_t *pixmap = window->pixmap; + twin_title_t t; + twin_coord_t lx = event->u.pointer.x; + _title_metrics(window, &t); + if (lx > twin_fixed_to_int(t.close_x) && + lx < twin_fixed_to_int(t.close_x + t.icon_size)) { + twin_window_close(window); + return true; + } + if (lx > twin_fixed_to_int(t.iconify_x) && + lx < twin_fixed_to_int(t.restore_x)) { + _twin_pixmap_set_scan_height(pixmap, window->client.top); + } else if (lx > twin_fixed_to_int(t.restore_x) && + lx < twin_fixed_to_int(t.close_x)) { + _twin_pixmap_set_scan_height(pixmap, pixmap->height); + } else { + twin_time_t now = twin_now(); + if (d->click == window && now - d->click_time <= DOUBLE_CLICK_MS) { + d->click = NULL; + _toggle_maximize(window); + /* Configure handlers may have destroyed it */ + if (!window->dead) + _twin_wm_map(window); + return true; + } + d->click = window; + d->click_time = now; + } + return false; +} + +/* Apply the size a resize drag asked for; at most once per frame, since + * each new size reallocates the window's pixmap */ +static void _resize_apply(twin_desktop_t *d) +{ + twin_window_t *w = d->resize; + if (d->resize_work) + twin_clear_work(d->resize_work); + d->resize_work = NULL; + if (!w || (d->resize_w == w->client.right - w->client.left && + d->resize_h == w->client.bottom - w->client.top)) + return; + w->wm_maximized = false; + _set_frame(w, w->request.left, w->request.top, d->resize_w, d->resize_h); +} + +static bool _resize_work(void *closure) +{ + twin_desktop_t *d = DESKTOP((twin_screen_t *) closure); + d->resize_work = NULL; /* the work item ends with this call */ + _resize_apply(d); + return false; +} + +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event) +{ + twin_screen_t *screen = window->screen; + twin_desktop_t *d = DESKTOP(screen); + /* Only pointer events carry coordinates */ + bool pointer = event->kind == TwinEventButtonDown || + event->kind == TwinEventButtonUp || + event->kind == TwinEventMotion; + bool in_client = pointer && window->client.left <= event->u.pointer.x && + event->u.pointer.x < window->client.right && + window->client.top <= event->u.pointer.y && + event->u.pointer.y < window->client.bottom; + /* Client callbacks may destroy the window (window->dead) */ + + switch (event->kind) { + case TwinEventActivate: + case TwinEventDeactivate: + /* Focus follows the WM's own bookkeeping; tell the client only. */ + return _twin_window_deliver(window, event); + case TwinEventButtonDown: + if (window->style == TwinWindowApplication && + event->u.pointer.y < window->client.top) { + if (_title_press(window, event)) + return true; + } else { + d->click = NULL; + } + _twin_wm_map(window); /* raise and activate */ + if (window->dead) + return true; + if (in_client && !_iconified(window)) { + window->client_grab = true; + if (_twin_window_deliver(window, event) || window->dead) + return true; + /* Unclaimed press inside the client drags the window */ + window->client_grab = false; + if (_in_grip(window, event->u.pointer.x, event->u.pointer.y)) { + d->resize = window; + d->button_x = event->u.pointer.screen_x - + (window->client.right - window->client.left); + d->button_y = event->u.pointer.screen_y - + (window->client.bottom - window->client.top); + return true; + } + } + if (window->style == TwinWindowApplication) { + d->drag = window; + d->button_x = event->u.pointer.x; + d->button_y = event->u.pointer.y; + } + return true; + case TwinEventButtonUp: + if (d->resize == window) { + _resize_apply(d); /* the final size, at once */ + d->resize = NULL; + return true; + } + if (d->drag == window) { + d->drag = NULL; + if (!window->client_grab) { + _drop(window, event->u.pointer.screen_x, + event->u.pointer.screen_y); + return true; + } + } + if (window->client_grab) { + window->client_grab = false; + return _twin_window_deliver(window, event); + } + return false; + case TwinEventMotion: + if (d->resize == window) { + d->resize_w = + max((twin_coord_t) (event->u.pointer.screen_x - d->button_x), + (twin_coord_t) MIN_CLIENT_W); + d->resize_h = + max((twin_coord_t) (event->u.pointer.screen_y - d->button_y), + (twin_coord_t) MIN_CLIENT_H); + if (!d->resize_work) + d->resize_work = + twin_set_work(_resize_work, TWIN_WORK_LAYOUT, screen); + if (!d->resize_work) + _resize_apply(d); /* out of memory: apply right away */ + return true; + } + if (d->drag == window && !window->client_grab) { + /* Dragging a maximized window puts it back to its own size, + * still under the pointer */ + if (window->wm_maximized) { + twin_coord_t old_w = window->pixmap->width; + _unmaximize(window); + /* Configure handlers may have destroyed it */ + if (window->dead) + return true; + if (d->button_x >= window->pixmap->width) + d->button_x = d->button_x * window->pixmap->width / old_w; + } + twin_coord_t x = event->u.pointer.screen_x - d->button_x; + twin_coord_t y = event->u.pointer.screen_y - d->button_y; + twin_coord_t w = window->request.right - window->request.left; + twin_coord_t h = window->request.bottom - window->request.top; + _clamp_title(screen, window->pixmap->width, &x, &y); + window->request = (twin_rect_t) {x, x + w, y, y + h}; + twin_pixmap_move(window->pixmap, x, y); + return true; + } + if (window->client_grab || in_client) + return _twin_window_deliver(window, event); + return false; + default: + return _twin_window_deliver(window, event); + } +} + +/* Shift @w's requested frame so it lies on screen where it fits */ +static void _clamp_to_screen(twin_window_t *w) +{ + twin_screen_t *screen = w->screen; + twin_rect_t geom, border; + _twin_wm_layout(w, &geom, &border); + twin_coord_t dx = 0, dy = 0; + if (geom.right > screen->width) + dx = max((twin_coord_t) (screen->width - geom.right), + (twin_coord_t) -geom.left); + if (geom.bottom > screen->height) + dy = max((twin_coord_t) (screen->height - geom.bottom), + (twin_coord_t) -geom.top); + /* Never leave the title bar above the screen */ + if (geom.top + dy < 0) + dy = (twin_coord_t) -geom.top; + if (geom.left + dx < 0 && geom.right + dx <= 0) + dx = (twin_coord_t) -geom.left; + _offset(&w->request, dx, dy); +} + +void _twin_wm_init(twin_screen_t *screen) +{ + (void) screen; /* the zeroed state is the start state */ +} + +/* The screen or the work area changed size: refit maximized windows and + * bring everything else back on screen */ +void _twin_wm_refresh(twin_screen_t *screen) +{ + twin_rect_t area = twin_screen_work_area(screen); + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (!w->pixmap || w->style != TwinWindowApplication) + continue; + if (w->wm_maximized) { + /* Snapped halves keep their side, maximized windows refit */ + twin_rect_t r = area; + twin_coord_t fw = w->pixmap->width; + twin_coord_t half = (area.right - area.left) / 2; + if (fw < area.right - area.left) { + if (w->pixmap->x > area.left) + r.left = r.right - half; + else + r.right = r.left + half; + } + _fill_area(w, r); + continue; + } + _clamp_to_screen(w); + _twin_window_relayout(w); /* no-op unless it moved */ + } +} + +void _twin_wm_fini(twin_screen_t *screen) +{ + twin_desktop_t *d = DESKTOP(screen); + if (d->resize_work) + twin_clear_work(d->resize_work); + d->resize_work = NULL; + d->drag = d->resize = d->click = NULL; +} diff --git a/src/wm-mobile.c b/src/wm-mobile.c new file mode 100644 index 00000000..0142b145 --- /dev/null +++ b/src/wm-mobile.c @@ -0,0 +1,609 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Mobile window manager policy + * + * Application windows form a stack over the work area, the region between + * the system bars. Only the top application is visible, so the policy + * releases the pixels of every application underneath it (eviction) and + * asks the application to repaint when it comes back. Steady state costs + * one work-area surface plus the bars, no matter how deep the stack is. + * + * Transitions never repaint applications. They move retained pixmaps and + * change their opacity, and the compositor does the rest: + * + * - push: the new app slides in from the right over the old one, which + * drifts left by a third and dims (opacity over black). + * - pop: the reverse. The departing window's pixmap is adopted when + * the application destroys it, so app state can be freed at once. + * - dismiss: a full-screen window (lock screen) slides up and away. + * - swipe: dragging from the left edge pops interactively; releasing past + * a third of the width asks the top window to go back + * (twin_window_back: unhandled, it closes). A press at the edge + * is held until the finger moves: a sideways drag starts the + * swipe, anything else (a tap, a vertical drag) is replayed to + * the application, so controls along the edge keep working. + * + * Escape that the top application does not consume also goes back. + * + * Every arrangement ends in mobile_settle(), which derives the visible set + * from the window list alone. Transitions only perturb positions and + * opacity in between, so interrupting one is always safe: finish the tween, + * settle, start the next. + */ + +#include + +#include "twin_private.h" + +#define EDGE_WIDTH 16 +/* Sideways travel that turns an edge press into a swipe, and how long a + * press may rest at the edge before it is handed to the app */ +#define EDGE_SLOP 8 +#define EDGE_HOLD_MS 150 +#define PUSH_MS 240 +#define POP_MS 220 +#define DISMISS_MS 260 +#define CANCEL_MS 160 +#define DIM_OPACITY 0x99 +#define SLIDE_SHADOW_BLUR 12 +#define SLIDE_SHADOW_ALPHA 0x60 + +typedef enum { + MOBILE_IDLE, + MOBILE_PUSH, /* front slides in over back */ + MOBILE_POP, /* orphan slides out, back is revealed */ + MOBILE_DISMISS, /* orphan slides up */ + MOBILE_SWIPE, /* finger drives a pop of front over back */ + MOBILE_CANCEL, /* swipe released early: front slides home */ +} twin_mobile_kind_t; + +typedef struct { + twin_mobile_kind_t kind; + twin_tween_t *tween; + twin_fixed_t from; /* progress the running tween started at */ + twin_fixed_t progress; /* last applied progress */ + twin_window_t *front; /* window whose pixmap is in front */ + twin_window_t *back; /* window underneath */ + twin_pixmap_t *orphan; /* pixels of a destroyed window, still leaving */ + twin_coord_t swipe_x0; /* pointer x when the swipe began */ + twin_event_t edge; /* edge press held until the finger moves */ + bool edge_pending; /* edge holds a press not yet routed */ + twin_timeout_t *edge_timer; /* hands a resting edge press to the app */ + bool swallow; /* a press was dropped: drop until its release */ +} twin_mobile_t; + +#define MOBILE(screen) ((twin_mobile_t *) _twin_wm_state(screen)) + +const size_t _twin_wm_state_size = sizeof(twin_mobile_t); + +static bool _animate(void) +{ + return !(_twin_debug & TWIN_DEBUG_NO_ANIMATION); +} + +void _twin_wm_layout(twin_window_t *window, + twin_rect_t *geom, + twin_rect_t *border) +{ + twin_screen_t *screen = window->screen; + twin_coord_t h = window->request.bottom - window->request.top; + + *border = (twin_rect_t) {0, 0, 0, 0}; + if (window == screen->bar_top) { + *geom = (twin_rect_t) {0, screen->width, 0, h}; + } else if (window == screen->bar_bottom) { + *geom = (twin_rect_t) {0, screen->width, screen->height - h, + screen->height}; + } else if (window->style == TwinWindowApplication) { + *geom = twin_screen_work_area(screen); + } else if (window->style == TwinWindowFullscreen) { + *geom = (twin_rect_t) {0, screen->width, 0, screen->height}; + } else { + *geom = window->request; + } +} + +static bool _is_app(const twin_window_t *w) +{ + return w->style == TwinWindowApplication && !_twin_window_is_bar(w); +} + +/* Topmost mapped application window other than @skip */ +static twin_window_t *_top_app(twin_screen_t *screen, twin_window_t *skip) +{ + twin_window_t *top = NULL; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w != skip && w->mapped && _is_app(w)) + top = w; + } + return top; +} + +/* Mapped application directly below @above */ +static twin_window_t *_app_below(twin_screen_t *screen, twin_window_t *above) +{ + twin_window_t *below = NULL; + for (twin_window_t *w = screen->windows; w && w != above; w = w->wm_next) { + if (w->mapped && _is_app(w)) + below = w; + } + return below; +} + +static bool _fullscreen_mapped(twin_screen_t *screen) +{ + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w->mapped && w->style == TwinWindowFullscreen) + return true; + } + return false; +} + +static void _home_position(twin_window_t *window) +{ + twin_rect_t geom, border; + _twin_wm_layout(window, &geom, &border); + twin_pixmap_move(window->pixmap, geom.left, geom.top); +} + +/* Show @w directly above @lower (NULL: bottom), only if not already there */ +static twin_pixmap_t *_place(twin_window_t *w, twin_pixmap_t *lower) +{ + _twin_window_restore(w); + twin_pixmap_t *p = w->pixmap; + twin_pixmap_set_opacity(p, 0xff); + twin_pixmap_set_shadow(p, 0, 0, 0, 0); + _home_position(w); + if (!p->screen || p->down != lower) + twin_pixmap_show(p, w->screen, lower); + return p; +} + +/* + * Derive the visible arrangement from the window list. + * Bottom to top: top application, system bars, full-screen windows, plain + * popups. Everything else is hidden, and hidden applications are evicted. + */ +static void mobile_settle(twin_screen_t *screen) +{ + twin_window_t *top = _top_app(screen, NULL); + twin_window_t *focus = top; + twin_pixmap_t *lower = NULL; + + /* An opaque full-screen window (lock screen) hides the app and the bars + * completely: release them too, they repaint when uncovered. */ + bool covered = false; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + if (w->mapped && w->style == TwinWindowFullscreen && w->opaque) + covered = true; + + twin_screen_disable_update(screen); + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + /* A window being destroyed may already have lost its pixmap */ + if (!w->pixmap) + continue; + bool hidden = (_is_app(w) && (w != top || covered)) || + (covered && _twin_window_is_bar(w)); + if (hidden) { + twin_pixmap_hide(w->pixmap); +#if defined(CONFIG_WM_MOBILE_EVICT) + _twin_window_evict(w); +#endif + } else if (!w->mapped) { + twin_pixmap_hide(w->pixmap); + } + } + if (top && !covered) + lower = _place(top, NULL); + if (screen->bar_top && screen->bar_top->mapped && !covered) + lower = _place(screen->bar_top, lower); + if (screen->bar_bottom && screen->bar_bottom->mapped && !covered) + lower = _place(screen->bar_bottom, lower); + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w->mapped && w->style == TwinWindowFullscreen) { + lower = _place(w, lower); + focus = w; + } + } + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (w->mapped && w->style == TwinWindowPlain && !_twin_window_is_bar(w)) + lower = _place(w, lower); + } + if (focus && screen->active != focus->pixmap) + twin_screen_set_active(screen, focus->pixmap); + twin_screen_enable_update(screen); +} + +/* + * Apply transition progress @v (0..1). Positions are relative to the work + * area; the back window parallaxes by a third and dims while covered. + */ +static void _apply(twin_screen_t *screen, twin_fixed_t v) +{ + twin_mobile_t *m = MOBILE(screen); + twin_rect_t work = twin_screen_work_area(screen); + twin_coord_t width = work.right - work.left; + twin_coord_t shift = twin_fixed_to_int( + twin_fixed_mul(twin_int_to_fixed(width), v) + TWIN_FIXED_HALF); + twin_a8_t dim = + (twin_a8_t) (DIM_OPACITY + twin_fixed_to_int((0xff - DIM_OPACITY) * v)); + + m->progress = v; + switch (m->kind) { + case MOBILE_PUSH: { + /* v = 0: back fully visible; v = 1: front fully in */ + twin_pixmap_t *front = m->front->pixmap, *back = m->back->pixmap; + twin_pixmap_move(front, work.left + width - shift, work.top); + twin_pixmap_move(back, work.left - shift / 3, work.top); + twin_pixmap_set_opacity( + back, + (twin_a8_t) (0xff - twin_fixed_to_int((0xff - DIM_OPACITY) * v))); + break; + } + case MOBILE_POP: + case MOBILE_SWIPE: + case MOBILE_CANCEL: { + /* v = 0: front covers back; v = 1: front gone */ + twin_pixmap_t *front = + m->kind == MOBILE_POP ? m->orphan : m->front->pixmap; + twin_pixmap_t *back = m->back->pixmap; + twin_pixmap_move(front, work.left + shift, work.top); + twin_pixmap_move(back, work.left - (width - shift) / 3, work.top); + twin_pixmap_set_opacity(back, dim); + break; + } + case MOBILE_DISMISS: { + twin_coord_t lift = twin_fixed_to_int( + twin_fixed_mul(twin_int_to_fixed(screen->height), v)); + twin_pixmap_move(m->orphan, 0, -lift); + break; + } + default: + break; + } +} + +static void _transition_done(twin_screen_t *screen) +{ + twin_mobile_t *m = MOBILE(screen); + m->tween = NULL; + m->kind = MOBILE_IDLE; + m->front = m->back = NULL; + if (m->orphan) { + twin_pixmap_destroy(m->orphan); + m->orphan = NULL; + } + mobile_settle(screen); +} + +static bool _tween_step(twin_fixed_t value, void *closure) +{ + twin_screen_t *screen = closure; + twin_mobile_t *m = MOBILE(screen); + twin_fixed_t v; + + /* Map tween output onto [from, to] */ + if (m->kind == MOBILE_CANCEL) + v = m->from - twin_fixed_mul(m->from, value); + else + v = m->from + twin_fixed_mul(TWIN_FIXED_ONE - m->from, value); + _apply(screen, v); + if (value >= TWIN_FIXED_ONE) { + _transition_done(screen); + return false; + } + return true; +} + +static void _start(twin_screen_t *screen, + twin_mobile_kind_t kind, + twin_fixed_t from, + twin_time_t duration) +{ + twin_mobile_t *m = MOBILE(screen); + m->kind = kind; + m->from = from; + /* Shorten the remaining distance proportionally */ + twin_fixed_t remain = kind == MOBILE_CANCEL ? from : TWIN_FIXED_ONE - from; + duration = (twin_time_t) (((int64_t) duration * remain) >> 16); + _apply(screen, from); + m->tween = + twin_tween_start(duration, TwinEaseOutCubic, _tween_step, screen); + if (!m->tween) + _transition_done(screen); +} + +/* Complete whatever is running and settle */ +static void _finish(twin_screen_t *screen) +{ + twin_mobile_t *m = MOBILE(screen); + if (m->tween) + twin_tween_finish(m->tween); + else if (m->kind != MOBILE_IDLE) + _transition_done(screen); +} + +static void _shadow_on(twin_pixmap_t *p) +{ +#if defined(CONFIG_DROP_SHADOW) + twin_pixmap_set_shadow(p, 0, 0, SLIDE_SHADOW_BLUR, SLIDE_SHADOW_ALPHA); +#else + (void) p; +#endif +} + +void _twin_wm_map(twin_window_t *window) +{ + twin_screen_t *screen = window->screen; + twin_mobile_t *m = MOBILE(screen); + + _finish(screen); + if (_twin_window_is_bar(window)) { + /* A bar coming back takes its rows out of the work area again */ + _twin_screen_relayout(screen); + return; + } + if (!_is_app(window)) { + _twin_window_raise(window); + mobile_settle(screen); + return; + } + + /* The window list is the stack; a new window already sits at its end, + * so "already on top" has to mean "already on screen". */ + twin_window_t *prev = _top_app(screen, window); + bool was_top = + _twin_window_shown(window) && _top_app(screen, NULL) == window; + _twin_window_raise(window); + if (!_animate() || !prev || prev == window || was_top || + !prev->pixmap->screen || _fullscreen_mapped(screen)) { + mobile_settle(screen); + return; + } + + /* Push: bring the new app in above the previous one, under the bars */ + _twin_window_restore(window); + m->front = window; + m->back = prev; + twin_pixmap_show(window->pixmap, screen, prev->pixmap); + _shadow_on(window->pixmap); + /* Keys go to the app sliding in, not the one leaving */ + if (screen->active != window->pixmap) + twin_screen_set_active(screen, window->pixmap); + _start(screen, MOBILE_PUSH, 0, PUSH_MS); +} + +void _twin_wm_unmap(twin_window_t *window, bool destroying) +{ + twin_screen_t *screen = window->screen; + twin_mobile_t *m = MOBILE(screen); + twin_fixed_t from = 0; + + if (m->kind == MOBILE_SWIPE && m->front == window && destroying) { + /* Destroyed mid-swipe: continue from where the finger left it */ + from = m->progress; + m->kind = MOBILE_IDLE; + m->front = m->back = NULL; + } else { + _finish(screen); + } + + if (_twin_window_is_bar(window)) { + /* A hidden bar gives its rows to the applications */ + twin_pixmap_hide(window->pixmap); + _twin_screen_relayout(screen); + return; + } + + bool visible = _twin_window_shown(window); + twin_window_t *next = _is_app(window) ? _top_app(screen, window) : NULL; + bool pop = _is_app(window) && next && visible; + bool dismiss = window->style == TwinWindowFullscreen && visible; + + if (!destroying || !_animate() || !(pop || dismiss)) { + twin_pixmap_hide(window->pixmap); + mobile_settle(screen); + return; + } + + /* Adopt the pixels so the application can free its state now */ + twin_pixmap_t *orphan = window->pixmap; + window->pixmap = NULL; + orphan->window = NULL; + _twin_screen_forget(screen, orphan); + m->orphan = orphan; + mobile_settle(screen); + + /* Settling may have hidden the window we meant to reveal (an opaque + * full-screen window covers it): nothing to animate then. */ + if (pop && !_twin_window_shown(next)) { + m->orphan = NULL; + twin_pixmap_destroy(orphan); + return; + } + + if (pop) { + m->back = next; + twin_pixmap_show(orphan, screen, next->pixmap); + _shadow_on(orphan); + _start(screen, MOBILE_POP, from, POP_MS); + } else { + twin_pixmap_show(orphan, screen, screen->top); + _start(screen, MOBILE_DISMISS, 0, DISMISS_MS); + } +} + +/* Start the interactive pop of the top app; false if there is none below */ +static bool _swipe_begin(twin_screen_t *screen, twin_coord_t x0) +{ + twin_mobile_t *m = MOBILE(screen); + twin_window_t *front = _top_app(screen, NULL); + twin_window_t *back = front ? _app_below(screen, front) : NULL; + if (!back || !_twin_window_restore(back)) + return false; + m->kind = MOBILE_SWIPE; + m->front = front; + m->back = back; + m->swipe_x0 = x0; + twin_pixmap_show(back->pixmap, screen, NULL); + _shadow_on(front->pixmap); + _apply(screen, 0); + return true; +} + +/* Stop holding the edge press: drop it, or route it to the windows as if + * the filter had never seen it */ +static void _edge_end(twin_screen_t *screen, bool deliver) +{ + twin_mobile_t *m = MOBILE(screen); + if (m->edge_timer) + twin_clear_timeout(m->edge_timer); + m->edge_timer = NULL; + m->edge_pending = false; + if (!deliver) + return; + _twin_screen_busy(screen); + _twin_screen_route(screen, &m->edge); + _twin_screen_idle(screen); +} + +static twin_time_t _edge_hold_done(twin_time_t now, void *closure) +{ + (void) now; + twin_screen_t *screen = closure; + MOBILE(screen)->edge_timer = NULL; /* this timeout ends here */ + _edge_end(screen, true); + return -1; +} + +/* Edge swipe: drag from the left edge to go back */ +bool _twin_wm_filter(twin_screen_t *screen, twin_event_t *event) +{ + twin_mobile_t *m = MOBILE(screen); + twin_coord_t x = event->u.pointer.screen_x; + twin_coord_t y = event->u.pointer.screen_y; + twin_rect_t work = twin_screen_work_area(screen); + twin_coord_t width = work.right - work.left; + + /* The rest of a press that was dropped during a transition */ + if (m->swallow) { + if (event->kind == TwinEventButtonUp) + m->swallow = false; + return true; + } + + if (m->kind == MOBILE_SWIPE) { + twin_coord_t dx = + min((twin_coord_t) max((twin_coord_t) (x - m->swipe_x0), + (twin_coord_t) 0), + width); + twin_fixed_t v = + twin_fixed_div(twin_int_to_fixed(dx), twin_int_to_fixed(width)); + _apply(screen, v); /* a release continues the pop from here */ + if (event->kind == TwinEventButtonUp) { + if (v * 3 > TWIN_FIXED_ONE) + twin_window_back(m->front); + /* Still swiping: released early, or the window stayed */ + if (m->kind == MOBILE_SWIPE) + _start(screen, MOBILE_CANCEL, v, CANCEL_MS); + } + return true; + } + + if (m->edge_pending) { + twin_coord_t dx = x - m->edge.u.pointer.screen_x; + twin_coord_t dy = y - m->edge.u.pointer.screen_y; + if (event->kind == TwinEventMotion) { + if (dx > EDGE_SLOP && dx > abs(dy)) { + _edge_end(screen, false); + if (_swipe_begin(screen, m->edge.u.pointer.screen_x)) + return _twin_wm_filter(screen, event); + _edge_end(screen, true); + return false; + } + if (abs(dy) <= EDGE_SLOP && dx >= -EDGE_SLOP) + return true; /* not decided yet */ + } + /* A tap, or a drag that is not a swipe: it belongs to the app */ + _edge_end(screen, true); + return false; + } + + if (event->kind != TwinEventButtonDown) + return false; + /* Ignore presses while a transition runs: they would land on moving UI */ + if (m->tween) { + m->swallow = true; + return true; + } + if (x >= work.left + EDGE_WIDTH || y < work.top || y >= work.bottom || + !_animate() || _fullscreen_mapped(screen)) + return false; + twin_window_t *front = _top_app(screen, NULL); + if (!front || !_app_below(screen, front)) + return false; + m->edge = *event; + m->edge_pending = true; + /* A press resting at the edge (a long press) is the app's too */ + m->edge_timer = twin_set_timeout(_edge_hold_done, EDGE_HOLD_MS, screen); + return true; +} + +/* Escape the application leaves alone goes back, like the edge swipe */ +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event) +{ + if (_twin_window_deliver(window, event) || window->dead) + return true; + if (event->kind == TwinEventUcs4 && event->u.ucs4.ucs4 == 27 && + _is_app(window) && _app_below(window->screen, window)) { + twin_window_back(window); + return true; + } + return false; +} + +void _twin_wm_init(twin_screen_t *screen) +{ + screen->background_color = 0xff000000; +} + +/* A mapped window changed style or opacity: rebuild the arrangement */ +void _twin_wm_refresh(twin_screen_t *screen) +{ + _finish(screen); + mobile_settle(screen); +} + +void _twin_wm_fini(twin_screen_t *screen) +{ + twin_mobile_t *m = MOBILE(screen); + if (m->edge_timer) + twin_clear_timeout(m->edge_timer); + if (m->tween) + twin_tween_cancel(m->tween); + if (m->orphan) + twin_pixmap_destroy(m->orphan); + /* The windows go next, without the policy */ + *m = (twin_mobile_t) {0}; +} + +static twin_coord_t _bar_height(const twin_window_t *bar) +{ + if (!bar || !bar->mapped) + return 0; + return max((twin_coord_t) 0, + (twin_coord_t) (bar->request.bottom - bar->request.top)); +} + +/* Mapped system bars take their requested heights off the top and bottom + * (twin_screen_work_area() keeps the result from inverting) */ +void _twin_wm_work_area(const twin_screen_t *screen, twin_rect_t *area) +{ + area->top = _bar_height(screen->bar_top); + area->bottom -= _bar_height(screen->bar_bottom); +} diff --git a/src/wm-none.c b/src/wm-none.c new file mode 100644 index 00000000..e96906b5 --- /dev/null +++ b/src/wm-none.c @@ -0,0 +1,86 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * The 'none' window manager (WM_NONE), the minimal one: Plain windows map + * where the application asked, Application and Fullscreen windows cover + * the screen, nothing is decorated, and a button press focuses the window + * under the pointer. It costs a few hundred bytes of code and no state, + * which is what a single-app kiosk wants. + */ + +#include "twin_private.h" + +const size_t _twin_wm_state_size = 0; + +void _twin_wm_init(twin_screen_t *screen) +{ + (void) screen; +} + +void _twin_wm_fini(twin_screen_t *screen) +{ + (void) screen; +} + +void _twin_wm_layout(twin_window_t *window, + twin_rect_t *geom, + twin_rect_t *border) +{ + twin_screen_t *screen = window->screen; + *border = (twin_rect_t) {0, 0, 0, 0}; + if (window->style == TwinWindowPlain) + *geom = window->request; + else + *geom = (twin_rect_t) {0, screen->width, 0, screen->height}; +} + +void _twin_wm_map(twin_window_t *window) +{ + twin_screen_t *screen = window->screen; + + /* System bars belong to the mobile shell. */ + if (_twin_window_is_bar(window)) + return; + _twin_window_restore(window); + /* The window list follows the stacking order: focus moves use it */ + _twin_window_raise(window); + if (window->pixmap != screen->top) + twin_pixmap_show(window->pixmap, screen, screen->top); + /* The newest window covers the screen, so it takes the keyboard */ + if (screen->active != window->pixmap) + twin_screen_set_active(screen, window->pixmap); +} + +void _twin_wm_unmap(twin_window_t *window, bool destroying) +{ + twin_screen_t *screen = window->screen; + (void) destroying; + bool had_focus = screen->active && screen->active == window->pixmap; + twin_pixmap_hide(window->pixmap); + if (!had_focus) + return; + /* The keyboard goes to the topmost window still shown */ + twin_window_t *next = NULL; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + if (w != window && w->mapped && _twin_window_shown(w) && + !_twin_window_is_bar(w)) + next = w; + if (next) + twin_screen_set_active(screen, next->pixmap); +} + +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event) +{ + if (event->kind == TwinEventButtonDown && + window->screen->active != window->pixmap) { + twin_screen_set_active(window->screen, window->pixmap); + /* Activation handlers may have destroyed it */ + if (window->dead) + return true; + } + return _twin_window_deliver(window, event); +} diff --git a/src/wm.c b/src/wm.c new file mode 100644 index 00000000..8f3afb97 --- /dev/null +++ b/src/wm.c @@ -0,0 +1,128 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Screen-wide window management services, on top of whichever policy is + * built (wm-none.c, wm-desktop.c or wm-mobile.c) + */ + +#include "twin_private.h" + +void _twin_screen_relayout(twin_screen_t *screen) +{ + if (screen->closing) + return; + /* Configure handlers run here: keep destroyed windows readable */ + _twin_screen_busy(screen); + twin_screen_disable_update(screen); + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + if (w->pixmap) + _twin_window_relayout(w); + _twin_wm_refresh(screen); + twin_screen_enable_update(screen); + _twin_screen_idle(screen); +} + +void twin_screen_set_bars(twin_screen_t *screen, + twin_window_t *top, + twin_window_t *bottom) +{ + /* Bars live on this screen */ + if ((top && top->screen != screen) || (bottom && bottom->screen != screen)) + return; +#if !TWIN_WM_BARS + /* Only the mobile window manager has system bars: elsewhere the + * windows are simply hidden */ + if (top) + twin_window_hide(top); + if (bottom) + twin_window_hide(bottom); +#else + /* One window cannot be both bars */ + if (top && top == bottom) + bottom = NULL; + if (top == screen->bar_top && bottom == screen->bar_bottom) + return; + twin_window_t *old[2] = {screen->bar_top, screen->bar_bottom}; + screen->bar_top = top; + screen->bar_bottom = bottom; + + /* Re-place everything: the work area may have changed size. The + * policy stays in place (no leave/enter), so this is safe from an + * event handler. A bar that is no longer registered is unmapped: + * left mapped, it would come back as a stray popup. */ + for (int i = 0; i < 2; i++) { + if (old[i] && old[i] != top && old[i] != bottom) + twin_window_hide(old[i]); + } + _twin_screen_relayout(screen); +#endif +} + +void twin_screen_set_inset(twin_screen_t *screen, twin_coord_t bottom) +{ +#if TWIN_WM_BARS + if (bottom < 0) + bottom = 0; + if (bottom == screen->inset_bottom) + return; + screen->inset_bottom = bottom; + _twin_screen_relayout(screen); +#else + /* Application windows cover the screen or are placed by the user */ + (void) screen; + (void) bottom; +#endif +} + +twin_rect_t twin_screen_work_area(const twin_screen_t *screen) +{ + twin_rect_t area = {0, screen->width, 0, screen->height}; +#if TWIN_WM_BARS + _twin_wm_work_area(screen, &area); + area.bottom -= screen->inset_bottom; + if (area.bottom < area.top) + area.bottom = area.top; +#endif + return area; +} + +void twin_window_activate(twin_window_t *window) +{ + if (!window->mapped || _twin_window_is_bar(window) || + window->screen->closing) + return; + /* Every policy raises and focuses a window it maps */ + _twin_wm_activate(window); +} + +void twin_window_back(twin_window_t *window) +{ + twin_screen_t *screen = window->screen; + twin_event_t ev = {.kind = TwinEventBack}; + /* The handler may destroy the window: keep it readable until we're out */ + _twin_screen_busy(screen); + bool handled = _twin_window_deliver(window, &ev); + /* The handler may have destroyed the window instead of handling it */ + if (!handled && !window->dead) + twin_window_close(window); + _twin_screen_idle(screen); +} + +void twin_screen_focus_next(twin_screen_t *screen) +{ + /* Raising the bottom window each time walks the whole stack */ + twin_window_t *bottom = NULL, *top = NULL; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + if (_twin_window_managed(w)) { + if (!bottom) + bottom = w; + top = w; + } + } + if (bottom && bottom != top) + twin_window_activate(bottom); +} diff --git a/src/work.c b/src/work.c index fca6fb57..f65dccfd 100644 --- a/src/work.c +++ b/src/work.c @@ -73,9 +73,14 @@ void _twin_run_work(void) if (running_depth++ == 0) { first = (twin_work_t *) _twin_queue_set_order(&head); for (work = first; work; work = (twin_work_t *) work->queue.order) { - /* Validate closure pointer using closure lifetime tracking */ - if (!_twin_closure_is_valid(work->closure)) + /* Cleared earlier in this pass (its closure is gone too) */ + if (work->queue.deleted) continue; + /* Owner gone: drop the item instead of skipping it forever */ + if (work->closure && !_twin_closure_is_valid(work->closure)) { + _twin_delete_work(work); + continue; + } if (!(*work->proc)(work->closure)) _twin_delete_work(work); @@ -100,8 +105,10 @@ void _twin_run_work(void) work = snapshot[i]; if (work->queue.deleted) continue; - if (!_twin_closure_is_valid(work->closure)) + if (work->closure && !_twin_closure_is_valid(work->closure)) { + _twin_delete_work(work); continue; + } if (!(*work->proc)(work->closure)) _twin_delete_work(work); @@ -132,13 +139,11 @@ twin_work_t *twin_set_work(twin_work_proc_t work_proc, /* Register closure with lifetime tracking if provided */ if (closure) { + /* One reference per pending work item, dropped on release */ if (!_twin_closure_register(closure)) { - /* Failed to register closure */ twin_free(work); return NULL; } - /* Add reference for this work item */ - _twin_closure_ref(closure); } work->proc = work_proc; diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt new file mode 100644 index 00000000..923bad77 --- /dev/null +++ b/tests/golden/cellphone.txt @@ -0,0 +1,60 @@ +# Golden frame checksums for the cellphone suite. +# Regenerate with: make check-update +# config 2aae645b +lock 4c07fb5b +home c7c79dd3 +calculator de05315a +dialer-empty 3a316e18 +dialer-number f321e819 +dialer-long 07be04fb +dialer-incall 319823bf +dialer-incall-unknown b582d3db +calllog 44f3bec9 +calllog-end 2dd522e7 +calllog-call 0892d459 +contacts-list 7331899b +contacts-list-scrolled de88e923 +contacts-detail 0e06fc6d +contacts-form-keyboard 918d84c4 +contacts-list-added 4c9b17f0 +contacts-form-validation cb34850f +contacts-form-full ce7bce9f +sms-list 343815e9 +sms-chat ede297fc +sms-typing e0fbccfb +sms-sent 353b92e1 +sms-chat-dark fa3fc17c +music-list c416fdb3 +music-now-playing ea9f6922 +music-paused 493cea51 +music-seeked 507dd579 +music-repeat 8f584c34 +music-shuffle 0567212d +music-now-playing-dark bf354c9e +music-list-playing 8503b11f +music-list-scrolled 7a3b5121 +music-list-reopened 9085b5b5 +photos-grid 1f4043e5 +photos-viewer bf0917c3 +photos-viewer-first 76b2f0b1 +photos-viewer-bare d6a2ef66 +camera 72b7aed9 +camera-shot 7960b270 +camera-video 4fcf0447 +camera-selfie b0422fb5 +settings 3c91c3d5 +settings-display 0b92e8d4 +settings-sound 8d73bf72 +settings-wallpaper afd4cf19 +settings-time 4732807f +settings-display-dark 873ef052 +settings-home-dark fa8cca66 +settings-sound-changed b5a3df17 +settings-wallpaper-dusk f9b149d2 +games-paused 3217c309 +games-snake ce3b429d +games-pong 52be2306 +games-tetris 8b239463 +games-tetris-late 4d439a8e +games-tetris-revealed f798a179 +home-demos 93c579a9 diff --git a/tests/golden/core.txt b/tests/golden/core.txt new file mode 100644 index 00000000..b37d3f22 --- /dev/null +++ b/tests/golden/core.txt @@ -0,0 +1,4 @@ +# Golden frame checksums for the core suite. +# Regenerate with: make check-update +# config 2aae645b +scene 373a3045 diff --git a/tests/harness.c b/tests/harness.c new file mode 100644 index 00000000..bb666d36 --- /dev/null +++ b/tests/harness.c @@ -0,0 +1,513 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include "harness.h" + +#include +#include + +int th_checks, th_failures; + +static twin_time_t virtual_now = 1000; + +static twin_time_t th_now(void) +{ + return virtual_now; +} + +static void th_put_span(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + th_t *t = closure; + memcpy(t->fb + (size_t) top * t->width + left, pixels, + sizeof(twin_argb32_t) * (size_t) (right - left)); +} + +th_t *th_create(twin_coord_t width, twin_coord_t height) +{ + th_t *t = calloc(1, sizeof(*t)); + if (!t) + return NULL; + _twin_set_time_source(th_now); + t->width = width; + t->height = height; + t->fb = calloc((size_t) width * height, sizeof(twin_argb32_t)); + t->screen = twin_screen_create(width, height, NULL, th_put_span, t); + if (!t->fb || !t->screen) { + if (t->screen) + twin_screen_destroy(t->screen); + free(t->fb); + free(t); + return NULL; + } + th_frame(t); + return t; +} + +void th_destroy(th_t *t) +{ + if (!t) + return; + twin_screen_destroy(t->screen); + /* Let deferred frees and orphaned timeouts drain */ + _twin_run_timeout(); + _twin_run_work(); + free(t->fb); + free(t); +} + +void th_resize(th_t *t, twin_coord_t width, twin_coord_t height) +{ + twin_argb32_t *fb = calloc((size_t) width * height, sizeof(*fb)); + if (!fb) + return; + free(t->fb); + t->fb = fb; + t->width = width; + t->height = height; + twin_screen_resize(t->screen, width, height); +} + +void th_frame(th_t *t) +{ + _twin_run_timeout(); + _twin_run_work(); + if (twin_screen_damaged(t->screen)) + twin_screen_update(t->screen); +} + +void th_advance(th_t *t, int ms) +{ + while (ms > 0) { + int step = ms < 16 ? ms : 16; + virtual_now += step; + ms -= step; + th_frame(t); + } + th_frame(t); +} + +void th_settle(th_t *t, int max_ms) +{ + for (int spent = 0; spent < max_ms;) { + th_frame(t); + twin_time_t delay = _twin_timeout_delay(); + if (delay < 0 || delay > max_ms - spent) + break; + /* Never step past the budget */ + int step = max_ms - spent < 16 ? max_ms - spent : 16; + virtual_now += step; + spent += step; + } + th_frame(t); +} + +static void th_pointer(th_t *t, + twin_event_kind_t kind, + twin_coord_t x, + twin_coord_t y) +{ + twin_event_t ev = {.kind = kind}; + ev.u.pointer.screen_x = x; + ev.u.pointer.screen_y = y; + ev.u.pointer.button = 1; + twin_screen_dispatch(t->screen, &ev); + th_frame(t); +} + +void th_press(th_t *t, twin_coord_t x, twin_coord_t y) +{ + th_pointer(t, TwinEventButtonDown, x, y); +} + +void th_move(th_t *t, twin_coord_t x, twin_coord_t y) +{ + th_pointer(t, TwinEventMotion, x, y); +} + +void th_release(th_t *t, twin_coord_t x, twin_coord_t y) +{ + th_pointer(t, TwinEventButtonUp, x, y); +} + +void th_tap(th_t *t, twin_coord_t x, twin_coord_t y) +{ + th_press(t, x, y); + th_advance(t, 32); + th_release(t, x, y); +} + +void th_drag(th_t *t, + twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1, + int steps) +{ + th_press(t, x0, y0); + for (int i = 1; i <= steps; i++) { + virtual_now += 16; + th_move(t, x0 + (x1 - x0) * i / steps, y0 + (y1 - y0) * i / steps); + } + th_release(t, x1, y1); +} + +void th_key(th_t *t, twin_ucs4_t ucs4) +{ + twin_event_t ev = {.kind = TwinEventUcs4}; + ev.u.ucs4.ucs4 = ucs4; + twin_screen_dispatch(t->screen, &ev); + th_frame(t); +} + +static uint32_t crc_table[256]; + +static void crc_init(void) +{ + if (crc_table[1]) + return; + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int k = 0; k < 8; k++) + c = (c & 1) ? 0xedb88320u ^ (c >> 1) : c >> 1; + crc_table[i] = c; + } +} + +static uint32_t crc_update(uint32_t crc, const void *data, size_t len) +{ + const uint8_t *p = data; + crc_init(); + while (len--) + crc = crc_table[(crc ^ *p++) & 0xff] ^ (crc >> 8); + return crc; +} + +static uint32_t th_crc_bytes(const void *data, size_t len) +{ + return ~crc_update(~0u, data, len); +} + +uint32_t th_crc_rect(const th_t *t, twin_rect_t r) +{ + uint32_t crc = ~0u; + for (twin_coord_t y = r.top; y < r.bottom; y++) + crc = crc_update(crc, t->fb + (size_t) y * t->width + r.left, + sizeof(twin_argb32_t) * (size_t) (r.right - r.left)); + return ~crc; +} + +uint32_t th_crc(const th_t *t) +{ + return th_crc_rect(t, (twin_rect_t) {0, t->width, 0, t->height}); +} + +twin_argb32_t th_pixel(const th_t *t, twin_coord_t x, twin_coord_t y) +{ + return t->fb[(size_t) y * t->width + x]; +} + +bool th_painted(const th_t *t, const twin_pixmap_t *p) +{ + twin_coord_t right = p->x + p->width, bottom = p->y + p->height; + if (right > t->width) + right = t->width; + if (bottom > t->height) + bottom = t->height; + twin_argb32_t first = th_pixel(t, p->x, p->y); + for (twin_coord_t y = p->y; y < bottom; y++) + for (twin_coord_t x = p->x; x < right; x++) + if (th_pixel(t, x, y) != first) + return true; + return false; +} + +twin_window_t *th_top_app(twin_screen_t *screen, int *count) +{ + twin_window_t *top = NULL; + int n = 0; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + if (w->mapped && w->style == TwinWindowApplication) { + top = w; + n++; + } + if (count) + *count = n; + return top; +} + +int th_damage_check(th_t *t) +{ + size_t n = (size_t) t->width * t->height; + twin_argb32_t *saved = malloc(n * sizeof(*saved)); + if (!saved) + return -1; + + /* Bring the incremental frame up to date first */ + if (twin_screen_damaged(t->screen)) + twin_screen_update(t->screen); + memcpy(saved, t->fb, n * sizeof(*saved)); + + twin_screen_stats_t stats = t->screen->stats; + unsigned debug = _twin_debug; + _twin_debug |= TWIN_DEBUG_NO_OCCLUSION; + twin_screen_damage(t->screen, 0, 0, t->width, t->height); + twin_screen_update(t->screen); + _twin_debug = debug; + t->screen->stats = stats; + + int diff = 0; + for (size_t i = 0; i < n; i++) { + if (saved[i] != t->fb[i]) { + if (!diff) + fprintf(stderr, + " first stale pixel at (%zu,%zu): %08x != %08x\n", + i % t->width, i / t->width, saved[i], t->fb[i]); + diff++; + } + } + free(saved); + return diff; +} + +static bool th_save_ppm(const th_t *t, const char *path) +{ + FILE *f = fopen(path, "wb"); + if (!f) + return false; + fprintf(f, "P6\n%d %d\n255\n", t->width, t->height); + for (size_t i = 0; i < (size_t) t->width * t->height; i++) { + twin_argb32_t p = t->fb[i]; + uint8_t rgb[3] = {(uint8_t) (p >> 16), (uint8_t) (p >> 8), (uint8_t) p}; + fwrite(rgb, 1, 3, f); + } + fclose(f); + return true; +} + +size_t th_heap_raw(void) +{ + twin_memory_info_t info; + twin_memory_get_info(&info); + return info.current_bytes; +} + +static bool th_heap_measured(void) +{ +#if defined(CONFIG_MEMORY_STATS) + return true; +#else + static bool told; + if (!told) { + printf("memory checks not measured (MEMORY_STATS is off)\n"); + told = true; + } + return false; +#endif +} + +size_t th_heap_net(const th_t *t) +{ + /* Without allocator statistics every reading is zero, so the leak and + * budget checks compare 0 with 0: say so once instead of underflowing */ + if (!th_heap_measured()) + return 0; + twin_cache_stats_t cs; + twin_cache_get_stats(&cs); + size_t retained = cs.bytes; + /* Bounded per-screen reuse pools, not leaks */ + retained += (size_t) t->screen->path_cache_count * sizeof(twin_path_t); + for (int i = 0; i < t->screen->path_cache_count; i++) { + twin_path_t *p = t->screen->path_cache[i]; + if (p->points != p->inline_points) + retained += (size_t) p->size_points * sizeof(twin_spoint_t); + if (p->sublen != p->inline_sublen) + retained += (size_t) p->size_sublen * sizeof(int); + } + retained += (size_t) t->screen->span_cache_width * sizeof(twin_argb32_t); + return th_heap_raw() - retained; +} + +/* + * Golden checksums + * + * Pixel output depends on build options that change rendering (renderer, + * shadow geometry, fonts, surface format, the JPEG decoder), so the golden + * file records a fingerprint of those options and comparisons are skipped + * 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 config_fingerprint(void) +{ + char buf[256]; + snprintf(buf, sizeof(buf), + "builtin=%d shadow=%d/%d/%d mont=%d icons=%d rgb16=%d jpeg=%d", +#if defined(CONFIG_RENDERER_BUILTIN) + 1, +#else + 0, +#endif +#if defined(CONFIG_DROP_SHADOW) + CONFIG_HORIZONTAL_OFFSET, CONFIG_VERTICAL_OFFSET, + CONFIG_SHADOW_BLUR, +#else + 0, 0, 0, +#endif +#if defined(CONFIG_FONT_MONTSERRAT) + 1, +#else + 0, +#endif +#if defined(CONFIG_FONT_ICONS) + 1, +#else + 0, +#endif +#if defined(CONFIG_DEMO_CELLPHONE_RGB16) + 1, +#else + 0, +#endif +#if defined(CONFIG_LOADER_JPEG) + 1 +#else + 0 +#endif + ); + return th_crc_bytes(buf, strlen(buf)); +} + +#define MAX_GOLDEN 256 + +typedef struct { + char name[64]; + uint32_t crc; +} golden_t; + +static golden_t golden_new[MAX_GOLDEN]; +static int n_golden_new; +static const char *golden_suite; +static int golden_skipped; + +static const char *golden_dir(void) +{ + const char *d = getenv("MADO_GOLDEN_DIR"); + return d ? d : "tests/golden"; +} + +/* How a golden file's recorded configuration compares with this build */ +typedef enum { + GOLDEN_UNAVAILABLE, /* file missing, or no "# config" line */ + GOLDEN_OTHER_CONFIG, + GOLDEN_SAME_CONFIG, +} golden_config_t; + +static bool golden_lookup(const char *suite, + const char *name, + uint32_t *crc, + golden_config_t *config) +{ + char path[512], line[256]; + snprintf(path, sizeof(path), "%s/%s.txt", golden_dir(), suite); + FILE *f = fopen(path, "r"); + *config = GOLDEN_UNAVAILABLE; + if (!f) + return false; + bool found = false; + while (fgets(line, sizeof(line), f)) { + char key[64]; + unsigned v; + if (sscanf(line, "# config %x", &v) == 1) { + *config = v == config_fingerprint() ? GOLDEN_SAME_CONFIG + : GOLDEN_OTHER_CONFIG; + continue; + } + if (sscanf(line, "%63s %x", key, &v) == 2 && !strcmp(key, name)) { + *crc = v; + found = true; + } + } + fclose(f); + return found; +} + +void th_golden(const char *suite, const char *name, uint32_t crc, const th_t *t) +{ + const char *shots = getenv("MADO_SCREENSHOTS"); + char path[512]; + golden_suite = suite; + + if (shots && t) { + snprintf(path, sizeof(path), "%s/%s-%s.ppm", shots, suite, name); + th_save_ppm(t, path); + } + if (getenv("MADO_UPDATE_GOLDEN")) { + if (n_golden_new < MAX_GOLDEN) { + snprintf(golden_new[n_golden_new].name, sizeof(golden_new[0].name), + "%s", name); + golden_new[n_golden_new++].crc = crc; + } + return; + } + + uint32_t want = 0; + golden_config_t config; + bool found = golden_lookup(suite, name, &want, &config); + /* Skip only a known configuration mismatch; a missing or malformed + * file must not silently disable the comparison */ + if (config == GOLDEN_OTHER_CONFIG) { + golden_skipped++; + return; + } + if (config == GOLDEN_UNAVAILABLE) { + CHECK( + false, + "%s/%s.txt missing or lacks a config line (run make check-update)", + golden_dir(), suite); + return; + } + CHECK(found, "no golden checksum for %s/%s (run make check-update)", suite, + name); + if (found && want != crc) { + CHECK(want == crc, "%s/%s: crc %08x, golden %08x", suite, name, crc, + want); + if (t) { + snprintf(path, sizeof(path), "%s-%s.fail.ppm", suite, name); + th_save_ppm(t, path); + fprintf(stderr, " wrote %s\n", path); + } + } +} + +int th_report(const char *suite) +{ + if (getenv("MADO_UPDATE_GOLDEN") && n_golden_new) { + char path[512]; + snprintf(path, sizeof(path), "%s/%s.txt", golden_dir(), + golden_suite ? golden_suite : suite); + FILE *f = fopen(path, "w"); + if (f) { + fprintf(f, "# Golden frame checksums for the %s suite.\n", suite); + fprintf(f, "# Regenerate with: make check-update\n"); + fprintf(f, "# config %08x\n", config_fingerprint()); + for (int i = 0; i < n_golden_new; i++) + fprintf(f, "%s %08x\n", golden_new[i].name, golden_new[i].crc); + fclose(f); + printf("%s: wrote %d golden checksums to %s\n", suite, n_golden_new, + path); + } + } + if (golden_skipped) + printf( + "%s: %d golden comparisons skipped (build options 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 new file mode 100644 index 00000000..62883819 --- /dev/null +++ b/tests/harness.h @@ -0,0 +1,137 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Headless test harness + * + * Runs a screen entirely in-process: scan-out lands in a memory framebuffer, + * time is a virtual clock advanced by the test, and input is injected + * through twin_screen_dispatch(). Mado is integer-only, so with the built-in + * renderer every frame is bit-exact across hosts; tests can compare CRCs. + */ + +#ifndef _TWIN_TEST_HARNESS_H_ +#define _TWIN_TEST_HARNESS_H_ + +#include +#include +#include +#include +#include + +#include "twin_private.h" + +typedef struct { + twin_screen_t *screen; + twin_argb32_t *fb; /* width * height, written by scan-out */ + twin_coord_t width, height; +} th_t; + +/* Create a screen of the given size (under the window manager built in) */ +th_t *th_create(twin_coord_t width, twin_coord_t height); +void th_destroy(th_t *t); + +/* Resize the screen (rotation, host window resize) and its framebuffer */ +void th_resize(th_t *t, twin_coord_t width, twin_coord_t height); + +/* Run timeouts and work, then scan out pending damage (one frame) */ +void th_frame(th_t *t); + +/* Advance virtual time by @ms in 16 ms frames */ +void th_advance(th_t *t, int ms); + +/* Run frames (advancing time) until no timeout is due within @max_ms */ +void th_settle(th_t *t, int max_ms); + +/* Pointer input in screen coordinates */ +void th_press(th_t *t, twin_coord_t x, twin_coord_t y); +void th_move(th_t *t, twin_coord_t x, twin_coord_t y); +void th_release(th_t *t, twin_coord_t x, twin_coord_t y); +void th_tap(th_t *t, twin_coord_t x, twin_coord_t y); +void th_drag(th_t *t, + twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1, + int steps); +void th_key(th_t *t, twin_ucs4_t ucs4); + +/* CRC-32 of the framebuffer */ +uint32_t th_crc(const th_t *t); + +/* Pixel at (x, y) */ +twin_argb32_t th_pixel(const th_t *t, twin_coord_t x, twin_coord_t y); + +/* CRC-32 of the frame inside @r */ +uint32_t th_crc_rect(const th_t *t, twin_rect_t r); + +/* The frame under @p is not one flat colour: something painted it (a + * released window that nobody repainted comes back flat) */ +bool th_painted(const th_t *t, const twin_pixmap_t *p); + +/* Topmost mapped application window, and how many there are */ +twin_window_t *th_top_app(twin_screen_t *screen, int *count); + +/* + * Damage consistency: recomposite the whole screen from scratch with every + * optimization off and compare with the incrementally maintained frame. + * A mismatch means some change was not damaged (stale pixels on screen). + * Returns the number of differing pixels. + */ +int th_damage_check(th_t *t); + +/* Heap minus bounded reuse pools (coverage cache, path/mask/span caches) */ +size_t th_heap_net(const th_t *t); +/* The whole heap, pools included: what the allocator's peak is measured + * against. Without MEMORY_STATS, th_heap_net() reads 0 (with a one-time + * notice) so leak checks compare 0 with 0 instead of underflowing. */ +size_t th_heap_raw(void); + +/* + * Minimal check framework + */ +extern int th_checks, th_failures; + +#define CHECK(cond, ...) \ + do { \ + th_checks++; \ + if (!(cond)) { \ + th_failures++; \ + fprintf(stderr, "FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +/* Evaluates each operand once (they may have side effects). The + * temporaries carry a prefix no test uses, so they cannot shadow an + * identifier an operand names. */ +#define CHECK_EQ(a, b, what) \ + do { \ + long long th_eq_a_ = (long long) (a), th_eq_b_ = (long long) (b); \ + CHECK(th_eq_a_ == th_eq_b_, "%s: got %lld, expected %lld", what, \ + th_eq_a_, th_eq_b_); \ + } while (0) + +/* NUL-terminated strings; @a may be NULL */ +#define CHECK_STR(a, b, what) \ + do { \ + const char *th_str_a_ = (a), *th_str_b_ = (b); \ + CHECK(th_str_a_ && !strcmp(th_str_a_, th_str_b_), \ + "%s: got \"%s\", expected \"%s\"", what, \ + th_str_a_ ? th_str_a_ : "(null)", th_str_b_); \ + } while (0) + +/* Print the summary line and return the process exit status */ +int th_report(const char *suite); + +/* Golden checksums: tests/golden/.txt holds "name crc" lines */ +void th_golden(const char *suite, + const char *name, + uint32_t crc, + const th_t *t); + +#endif /* _TWIN_TEST_HARNESS_H_ */ diff --git a/tests/phone-contacts.c b/tests/phone-contacts.c new file mode 100644 index 00000000..f2c045db --- /dev/null +++ b/tests/phone-contacts.c @@ -0,0 +1,206 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Contacts scenario: scrolling and flinging the A-Z list, opening a contact + * card, and the new-contact form (soft keyboard, validation, capacity). + */ + +#include + +#include "data.h" +#include "phone-test.h" + +int app_contacts_test_offset(void); +const char *app_contacts_test_detail(void); +const char *app_contacts_test_form_error(void); +bool app_contacts_test_keyboard(void); + +/* Layout mirrored from contacts.c */ +#define LIST_TOP PHONE_HEADER_H + +/* Center y of stock contact @i (every stock initial is distinct) */ +static twin_coord_t row_y(int i) +{ + return LIST_TOP + (i + 1) * PHONE_SECTION_H + i * PHONE_LIST_ROW_H + + PHONE_LIST_ROW_H / 2 - app_contacts_test_offset(); +} + +static void type_hw(const char *s) +{ + for (; *s; s++) + th_key(phone_th, (twin_ucs4_t) *s); + ph_wait(100); +} + +/* Form geometry with the keyboard hidden */ +#define FIELD_BOX_Y(i) (PHONE_HEADER_H + 6 + (i) * 50 + 15 + 15) +#define SAVE_X 178 +#define SAVE_Y (PHONE_CONTENT_H - 44 + 20) +#define CANCEL_X 62 + +static void test_list_scroll(void) +{ + CHECK_EQ(app_contacts_test_offset(), 0, "list starts at the top"); + ph_shot("contacts-list"); + + /* Slow drag: pausing before lifting the finger means no fling */ + th_press(phone_th, 120, CY(200)); + for (int i = 1; i <= 10; i++) { + th_advance(phone_th, 16); + th_move(phone_th, 120, CY(200 - i * 10)); + } + th_advance(phone_th, 150); + th_release(phone_th, 120, CY(100)); + ph_wait(500); + CHECK_EQ(app_contacts_test_offset(), 100, "drag scrolls by its distance"); + CHECK_EQ(phone_depth(), 1, "a scroll is not a tap"); + ph_shot("contacts-list-scrolled"); + + /* Rows map to the right contact under a scroll offset */ + ph_tap(120, row_y(3)); + const char *name = app_contacts_test_detail(); + CHECK_STR(name, "Daniel Brown", "detail shows the contact"); + CHECK_EQ(phone_depth(), 2, "detail pushed"); + ph_shot("contacts-detail"); + + /* Call and Message hand off to other apps */ + ph_tap(42, 132); + CHECK_EQ(phone_depth(), 3, "call opens the dialer"); + phone_back(); + ph_wait(2000); + ph_tap(120, 132); + CHECK_EQ(phone_depth(), 3, "message opens messages"); + phone_back(); + ph_wait(2000); + CHECK(app_contacts_test_detail() != NULL, "back on the card"); + phone_back(); + ph_wait(2000); + CHECK_EQ(app_contacts_test_offset(), 100, "list keeps its scroll offset"); + + /* Fling: keeps moving after the finger lifts, then comes to rest */ + th_drag(phone_th, 120, CY(200), 120, CY(140), 4); + int o1 = app_contacts_test_offset(); + th_advance(phone_th, 64); + int o2 = app_contacts_test_offset(); + CHECK(o2 > o1, "fling continues: %d -> %d", o1, o2); + ph_wait(3000); + int o3 = app_contacts_test_offset(); + ph_wait(500); + CHECK(o3 > o2, "fling travelled: %d -> %d", o2, o3); + CHECK_EQ(app_contacts_test_offset(), o3, "fling stopped"); + CHECK_EQ(phone_depth(), 1, "fling is not a tap"); + CHECK_EQ(th_damage_check(phone_th), 0, "no stale pixels after fling"); + + /* Fling back to the top */ + for (int i = 0; i < 3 && app_contacts_test_offset(); i++) + ph_drag(120, 60, 120, 240); + CHECK_EQ(app_contacts_test_offset(), 0, "back at the top"); +} + +static void open_form(void) +{ + ph_tap(20, PHONE_HEADER_H / 2); /* add, at the header start */ + CHECK(app_contacts_test_form_error() != NULL, "form opened"); +} + +static void test_add(void) +{ + uint32_t n = phone_contact_count(); + + open_form(); + CHECK(!app_contacts_test_keyboard(), "keyboard hidden at first"); + ph_tap(120, FIELD_BOX_Y(0)); + CHECK(app_contacts_test_keyboard(), "focusing a field shows the keyboard"); + + /* Soft keys: names auto-capitalize */ + ph_key('z'); + ph_key('o'); + ph_key('e'); + ph_shot("contacts-form-keyboard"); + ph_key('\r'); /* Enter: on to the phone field, digits layout */ + ph_key('5'); + ph_key('5'); + ph_key('5'); + ph_key('-'); + type_hw("0199\r"); + type_hw("zoe@example.com\r"); + CHECK(!app_contacts_test_keyboard(), "Enter on the last field hides it"); + + ph_tap(SAVE_X, SAVE_Y); + CHECK_EQ(phone_contact_count(), n + 1, "contact saved"); + CHECK(app_contacts_test_offset() >= 0, "back on the list"); + const phone_contact_t *c = phone_contact_at(n); + CHECK(c && !strcmp(c->name, "Zoe") && !strcmp(c->phone, "555-0199") && + !strcmp(c->email, "zoe@example.com"), + "sorted last: %s / %s / %s", c ? c->name : "-", c ? c->phone : "-", + c ? c->email : "-"); + CHECK(app_contacts_test_offset() > 0, "list scrolled to the new contact"); + ph_shot("contacts-list-added"); + + /* Hardware keys type verbatim; sorting ignores case */ + open_form(); + ph_tap(120, FIELD_BOX_Y(0)); + type_hw("bella Hart"); + ph_tap(120, PHONE_HEADER_H / 2); + CHECK(!app_contacts_test_keyboard(), "tapping outside hides the keyboard"); + ph_tap(SAVE_X, SAVE_Y); + c = phone_contact_at(1); + CHECK_STR(c ? c->name : NULL, "bella Hart", "inserted after Ashley"); + + /* Validation: an empty (or blank) name is refused */ + n = phone_contact_count(); + open_form(); + ph_tap(120, FIELD_BOX_Y(0)); + type_hw(" "); + ph_tap(SAVE_X, PHONE_CONTENT_H - UI_KBD_H - 20); + const char *err = app_contacts_test_form_error(); + CHECK_STR(err, "Name required", "form error"); + CHECK_EQ(phone_contact_count(), n, "nothing added"); + CHECK(!app_contacts_test_keyboard(), "keyboard hidden to show the error"); + ph_shot("contacts-form-validation"); + + /* Typing clears the message; names stop at the storage limit */ + ph_tap(120, FIELD_BOX_Y(0)); + type_hw("\b\b\bMaximilian Alexander Montgomery-Smythe"); + err = app_contacts_test_form_error(); + CHECK(err && !*err, "error cleared by typing"); + ph_tap(120, PHONE_HEADER_H / 2); + ph_tap(SAVE_X, SAVE_Y); + CHECK_EQ(phone_contact_count(), n + 1, "long name saved"); + c = NULL; + for (uint32_t i = 0; i < phone_contact_count(); i++) + if (phone_contact_at(i)->name[0] == 'M') + c = phone_contact_at(i); + CHECK(c && strlen(c->name) == 31, "name capped at 31 chars: %zu", + c ? strlen(c->name) : (size_t) 0); + + /* Capacity: fill the store, then the form reports it */ + while (phone_contact_count() < PHONE_CONTACT_CAPACITY) + phone_contact_add("Yuri Filler", "", ""); + open_form(); + ph_tap(120, FIELD_BOX_Y(0)); + type_hw("Olivia\r"); + ph_tap(120, PHONE_HEADER_H / 2); + ph_tap(SAVE_X, SAVE_Y); + err = app_contacts_test_form_error(); + CHECK_STR(err, "Contact list full", "form error"); + CHECK_EQ(phone_contact_count(), PHONE_CONTACT_CAPACITY, "still full"); + ph_shot("contacts-form-full"); + + ph_tap(CANCEL_X, SAVE_Y); + CHECK(app_contacts_test_offset() >= 0, "cancel returns to the list"); + CHECK_EQ(phone_depth(), 1, "form closed"); +} + +void test_contacts(void) +{ + ph_open("Contacts"); + test_list_scroll(); + test_add(); + ph_home(); + phone_data_reset(); +} diff --git a/tests/phone-demos.c b/tests/phone-demos.c new file mode 100644 index 00000000..a288fbd4 --- /dev/null +++ b/tests/phone-demos.c @@ -0,0 +1,143 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Registry apps from the launcher: every app in apps.h marked for the + * mobile policy has a home-screen icon that opens the same program the + * desktop runs, stacked by the mobile window manager over the work area. + * Edge swipe and the nav bar's Back close its windows one at a time, the + * nav bar follows the stack, and nothing is left behind. + */ + +#include "phone-test.h" + +bool app_home_icon_rect(int app, ui_rect_t *out); + +static uint32_t nav_crc(void) +{ + return th_crc_rect( + phone_th, (twin_rect_t) {0, PHONE_W, PHONE_H - PHONE_NAV_H, PHONE_H}); +} + +/* Tap @app's icon; false when it is not on the page shown */ +static bool tap_icon(int app) +{ + ui_rect_t r; + if (!app_home_icon_rect(app, &r)) + return false; + ph_tap((r.left + r.right) / 2, (r.top + r.bottom) / 2); + return true; +} + +void test_demos(void) +{ + int first = -1, count = 0; + for (int i = 0; i < phone_app_count(); i++) + if (phone_app_at(i)->demo) { + if (first < 0) + first = i; + count++; + } + if (!count) + return; + + uint32_t home_nav = nav_crc(); + /* The demos follow the phone apps: page to the first one */ + ui_rect_t r; + int pages = 0; + while (!app_home_icon_rect(first, &r) && pages < 8) { + ph_drag(200, 120, 20, 120); + pages++; + } + /* Pressing tints the whole tile, so the whole tile is repainted: what + * the press drew matches a repaint of the entire view. Press a corner + * outside the icon, then drag straight down to cancel. */ + if (app_home_icon_rect(first, &r)) { + /* A margin past the tile catches damage that stops short of it */ + twin_rect_t tile = {r.left - 6, r.right + 6, CY(r.top - 6), + CY(r.bottom + 6)}; + th_press(phone_th, r.left + 3, CY(r.bottom - 3)); + ph_wait(50); + uint32_t pressed = th_crc_rect(phone_th, tile); + ui_view_invalidate_all(phone_top()); + ph_wait(50); + CHECK_EQ(th_crc_rect(phone_th, tile), pressed, + "pressed tile repainted whole"); + th_move(phone_th, r.left + 3, CY(r.bottom + 40)); + 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++) { + if (pass) + base = th_heap_net(phone_th); /* first pass warms the caches */ + for (int i = first; i < phone_app_count(); i++) { + const char *name = phone_app_at(i)->name; + if (!tap_icon(i)) + break; /* the icons below are tested on this page only */ + CHECK(phone_depth() > 0, "%s opened from the launcher", name); + CHECK(!phone_top(), "%s: its own window is on top", name); + twin_window_t *w = th_top_app(phone_screen(), NULL); + twin_pixmap_t *p = w ? w->pixmap : NULL; + CHECK(p && p->x == 0 && p->y == PHONE_STATUS_H && + p->width == PHONE_W && p->height == PHONE_CONTENT_H, + "%s fills the work area", name); + CHECK(nav_crc() != home_nav, "%s: nav bar offers Back", name); + ph_check(name); + + /* An on-screen keyboard shrinks the work area: the window + * follows and repaints at the new size, then grows back */ + for (int k = 0; k < 2; k++) { + twin_coord_t inset = k ? 0 : 96; + twin_screen_set_inset(phone_screen(), inset); + ph_wait(500); + p = w ? w->pixmap : NULL; + CHECK(p && p->height == PHONE_CONTENT_H - inset, + "%s: height with inset %d", name, inset); + CHECK(p && th_painted(phone_th, p), "%s: painted with inset %d", + name, inset); + CHECK_EQ(th_damage_check(phone_th), 0, name); + } + + /* The back gesture closes the window on top ... */ + int depth = phone_depth(); + ph_drag(2, 120, 210, 120); /* swipe from the left edge */ + CHECK_EQ(phone_depth(), depth - 1, name); + /* ... and the nav bar's Back closes the rest */ + for (int guard = 0; phone_depth() > 0 && guard < 16; guard++) { + phone_back(); + th_settle(phone_th, 2000); + } + CHECK_EQ(phone_depth(), 0, name); + CHECK(nav_crc() == home_nav, "%s: nav bar back to home", name); + CHECK_EQ(th_damage_check(phone_th), 0, name); + } + } +#if defined(CONFIG_MEMORY_STATS) + CHECK(th_heap_net(phone_th) <= base, "demos leak (%zu > %zu)", + th_heap_net(phone_th), base); +#else + (void) base; +#endif + + /* Home also closes every window a demo opened */ + phone_open(first); + th_settle(phone_th, 2000); + ph_home(); + + while (pages-- > 0) + ph_drag(20, 120, 200, 120); +} diff --git a/tests/phone-dialer.c b/tests/phone-dialer.c new file mode 100644 index 00000000..48d1c3e6 --- /dev/null +++ b/tests/phone-dialer.c @@ -0,0 +1,179 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* Phone dialer, in-call screen and Call Log scenarios */ + +#include + +#include "phone-test.h" + +const char *app_dialer_test_number(void); +const char *app_dialer_test_name(void); +int app_dialer_test_seconds(void); +int app_dialer_test_toggles(void); +int app_calllog_test_offset(void); + +/* Keypad geometry mirrors apps/cellphone/dialer.c (content coordinates) */ +#define KEY_X(col) ((col) * 80 + 40) +#define KEY_Y(row) (50 + (row) * 40 + 20) +#define DEL_X 41 +#define CALL_X 158 +#define BOTTOM_Y 234 +#define MUTE_X 48 +#define SPEAKER_X 192 +#define TOGGLE_Y 204 +#define END_X 120 +#define END_Y 204 + +static void tap_digit(char ch) +{ + static const char layout[] = "123456789*0#"; + const char *p = strchr(layout, ch); + CHECK(p && *p, "digit %c on the keypad", ch); + if (!p || !*p) + return; + int i = (int) (p - layout); + ph_tap(KEY_X(i % 3), KEY_Y(i / 3)); +} + +static bool number_is(const char *want) +{ + const char *got = app_dialer_test_number(); + return got && !strcmp(got, want); +} + +void test_dialer(void) +{ + ph_open("Phone"); + ph_shot("dialer-empty"); + + /* Calling with nothing dialed does nothing */ + ph_tap(CALL_X, BOTTOM_Y); + CHECK_EQ(phone_depth(), 1, "empty call stays on the keypad"); + ph_tap(DEL_X, BOTTOM_Y); + CHECK(number_is(""), "backspace on empty is harmless"); + + for (const char *s = "15550101"; *s; s++) + tap_digit(*s); + CHECK(number_is("15550101"), "typed digits: %s", + app_dialer_test_number() ? app_dialer_test_number() : "(null)"); + ph_shot("dialer-number"); + + ph_tap(DEL_X, BOTTOM_Y); + CHECK(number_is("1555010"), "backspace drops the last digit"); + tap_digit('2'); + tap_digit('*'); + tap_digit('#'); + CHECK(number_is("15550102*#"), "star and hash typed"); + + /* Hardware keys: backspace twice, then Enter places the call */ + th_key(phone_th, 8); + th_key(phone_th, 8); + ph_wait(100); + CHECK(number_is("15550102"), "keyboard backspace"); + + /* Non-ASCII code points must not alias onto keys: U+0131 narrows to + * '1' and U+012B to '+' */ + th_key(phone_th, 0x131); + th_key(phone_th, 0x12B); + ph_wait(100); + CHECK(number_is("15550102"), "non-ASCII keys ignored"); + + /* The number fills up at 20 digits and the display truncates */ + for (int i = 0; i < 14; i++) + th_key(phone_th, '9'); + ph_wait(100); + CHECK(number_is("15550102999999999999"), "number capped at 20 digits"); + ph_shot("dialer-long"); + for (int i = 0; i < 12; i++) + th_key(phone_th, 8); + ph_wait(100); + CHECK(number_is("15550102"), "trimmed back"); + + /* Call: the in-call screen pushes on top of the keypad */ + ph_tap(CALL_X, BOTTOM_Y); + CHECK_EQ(phone_depth(), 2, "call screen pushed"); + CHECK(number_is("15550102"), "call carries the dialed number"); + const char *name = app_dialer_test_name(); + CHECK_STR(name, "Brian Smith", "number resolved to a contact"); + int s0 = app_dialer_test_seconds(); + CHECK(s0 >= 0, "call timer running"); + ph_wait(3000); + int s1 = app_dialer_test_seconds(); + CHECK(s1 - s0 >= 2 && s1 - s0 <= 4, "timer advanced %d -> %d", s0, s1); + + ph_tap(MUTE_X, TOGGLE_Y); + CHECK_EQ(app_dialer_test_toggles(), 1, "mute on"); + ph_tap(SPEAKER_X, TOGGLE_Y); + CHECK_EQ(app_dialer_test_toggles(), 3, "speaker on"); + ph_tap(MUTE_X, TOGGLE_Y); + CHECK_EQ(app_dialer_test_toggles(), 2, "mute off"); + ph_shot("dialer-incall"); + + ph_tap(END_X, END_Y); + CHECK_EQ(phone_depth(), 1, "end call returns to the keypad"); + CHECK(number_is("15550102"), "keypad keeps the number after the call"); + + /* An unknown number shows "Unknown" with a generic avatar */ + for (int i = 0; i < 8; i++) + th_key(phone_th, 8); + for (const char *s = "0800123"; *s; s++) + th_key(phone_th, (twin_ucs4_t) *s); + th_key(phone_th, '\r'); + ph_wait(500); + CHECK_EQ(phone_depth(), 2, "Enter places the call"); + name = app_dialer_test_name(); + CHECK(name && !name[0], "unknown number has no contact name"); + CHECK(number_is("0800123"), "unknown number dialed"); + ph_shot("dialer-incall-unknown"); + + /* Nav-bar Back hangs up too */ + phone_back(); + ph_wait(2000); + CHECK_EQ(phone_depth(), 1, "back ends the call"); + ph_home(); +} + +void test_calllog(void) +{ + ph_open("Call Log"); + ph_shot("calllog"); + CHECK_EQ(app_calllog_test_offset(), 0, "list starts at the top"); + + /* Drag up: the list overflows (15 rows of 40 px in 228 px) */ + ph_drag(120, 220, 120, 80); + int off = app_calllog_test_offset(); + CHECK(off > 100, "drag scrolled the list to %d", off); + CHECK_EQ(phone_depth(), 1, "scrolling is not a tap"); + + /* Fling to the bottom, then check the last rows render cleanly */ + ph_drag(120, 240, 120, 40); + ph_drag(120, 240, 120, 40); + off = app_calllog_test_offset(); + CHECK_EQ(off, 15 * 40 - (258 - 30), "scrolled to the end"); + ph_shot("calllog-end"); + + /* Back to the top and tap the third row: the missed unknown caller */ + ph_drag(120, 40, 120, 250); + ph_drag(120, 40, 120, 250); + CHECK_EQ(app_calllog_test_offset(), 0, "back at the top"); + ph_tap(120, 30 + 2 * 40 + 20); + CHECK_EQ(phone_depth(), 2, "row tap opens the call screen"); + const char *num = app_dialer_test_number(); + CHECK_STR(num, "+1-555-9999", "number called"); + ph_tap(END_X, END_Y); + CHECK_EQ(phone_depth(), 1, "end call returns to the log"); + + /* First row: a known contact */ + ph_tap(120, 30 + 20); + const char *name = app_dialer_test_name(); + CHECK_STR(name, "Ashley Johnson", "contact called"); + CHECK(app_dialer_test_seconds() >= 0, "call timer running"); + ph_shot("calllog-call"); + ph_tap(END_X, END_Y); + CHECK_EQ(phone_depth(), 1, "hung up"); + ph_home(); +} diff --git a/tests/phone-games.c b/tests/phone-games.c new file mode 100644 index 00000000..85b5256a --- /dev/null +++ b/tests/phone-games.c @@ -0,0 +1,132 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Games: Snake, Pong and Tetris play themselves. Checks that they advance, + * pause and resume on tap, replay identically on every open, score, and + * that a tick composites only the cells it changed. + */ + +#include + +#include "phone-test.h" + +int app_games_test_steps(void); +int app_games_test_score(void); +int app_games_test_best(void); +int app_games_test_events(void); +int app_games_test_full(void); +bool app_games_test_paused(void); + +/* A full content-area frame; a tick must stay well below it */ +#define FULL_FRAME (PHONE_W * PHONE_CONTENT_H) + +typedef struct { + int steps, score, best, events; +} snapshot_t; + +static snapshot_t snap(void) +{ + return (snapshot_t) {app_games_test_steps(), app_games_test_score(), + app_games_test_best(), app_games_test_events()}; +} + +/* Pixels scanned out per tick over a second without full-board repaints */ +static void check_pixels_per_tick(const char *name) +{ + for (int attempt = 0; attempt < 8; attempt++) { + int full = app_games_test_full(); + int steps = app_games_test_steps(); + uint64_t pixels = phone_th->screen->stats.pixels; + ph_wait(1000); + int ticks = app_games_test_steps() - steps; + if (app_games_test_full() != full) + continue; /* a restart or line clear repaints the board */ + CHECK(ticks > 0, "%s ticks during the window", name); + if (ticks <= 0) + return; + uint64_t per = (phone_th->screen->stats.pixels - pixels) / ticks; + printf(" %s: %llu px/tick over %d ticks (%llu.%llu%% of a frame)\n", + name, (unsigned long long) per, ticks, + (unsigned long long) (per * 100 / FULL_FRAME), + (unsigned long long) (per * 1000 / FULL_FRAME % 10)); + CHECK(per < FULL_FRAME / 4, "%s: %llu px per tick", name, + (unsigned long long) per); + return; + } + CHECK(0, "%s: no window without a full repaint", name); +} + +static void check_pause(const char *name) +{ + ph_tap(PHONE_W / 2, 130); + CHECK(app_games_test_paused(), "%s paused by tap", name); + int steps = app_games_test_steps(); + ph_wait(1500); + CHECK_EQ(app_games_test_steps(), steps, "pause freezes the game"); + if (!strcmp(name, "Snake")) + ph_shot("games-paused"); + + ph_tap(PHONE_W / 2, 130); + CHECK(!app_games_test_paused(), "%s resumed by tap", name); + CHECK(app_games_test_steps() > steps, "%s continues after resume", name); +} + +static void play(const char *name, const char *shot) +{ + ph_open(name); + CHECK_EQ(phone_depth(), 1, "game opened"); + int steps = app_games_test_steps(); + CHECK(steps > 0, "%s ticked while opening", name); + ph_wait(2000); + CHECK(app_games_test_steps() > steps, "%s advances", name); + snapshot_t a = snap(); + + check_pixels_per_tick(name); + check_pause(name); + ph_shot(shot); + + /* Reopen: the per-open seed replays the same game */ + ph_home(); + ph_open(name); + ph_wait(2000); + snapshot_t b = snap(); + CHECK(!memcmp(&a, &b, sizeof(a)), + "%s replays: steps %d/%d score %d/%d events %d/%d", name, a.steps, + b.steps, a.score, b.score, a.events, b.events); +} + +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(); + + play("Pong", "games-pong"); + ph_wait(5000); + CHECK(app_games_test_events() > 0, "pong paddles hit the ball"); + ph_home(); + + 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"); + + ph_home(); +} diff --git a/tests/phone-music.c b/tests/phone-music.c new file mode 100644 index 00000000..79f72613 --- /dev/null +++ b/tests/phone-music.c @@ -0,0 +1,188 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Music: track list, now playing, simulated playback, seeking, track + * roll-over, shuffle/repeat, and the cost of a playback tick. + */ + +#include + +#include "data.h" +#include "phone-test.h" + +int app_music_test_elapsed(void); +int app_music_test_track(void); +bool app_music_test_playing(void); +int app_music_test_scroll(void); + +/* Now-playing layout (content coordinates) */ +#define CTL_Y 230 +#define X_SHUFFLE 28 +#define X_PREV 74 +#define X_PLAY 120 +#define X_NEXT 166 +#define X_REPEAT 212 +#define SLIDER_Y 187 +#define SLIDER_X0 52 /* value 0 */ +#define SLIDER_X1 188 /* value 1000 */ + +/* Track list layout */ +#define LIST_TOP 30 +#define ROW_H 40 +#define MINI_Y 210 + +static int duration(int track) +{ + return (int) phone_tracks()[track].duration_sec; +} + +static void test_now_playing(void) +{ + /* Tap the third song */ + ph_tap(120, LIST_TOP + 2 * ROW_H + ROW_H / 2); + CHECK_EQ(phone_depth(), 2, "now playing pushed"); + CHECK_EQ(app_music_test_track(), 2, "tapped track selected"); + CHECK(app_music_test_playing(), "tapping a song plays it"); + ph_shot("music-now-playing"); + + /* Simulated playback advances with (virtual) time */ + int e0 = app_music_test_elapsed(); + ph_wait(3000); + int e1 = app_music_test_elapsed(); + CHECK(e1 - e0 >= 2 && e1 - e0 <= 4, "3 s of playback advanced %d s", + e1 - e0); + + /* A playback tick repaints only the knob and the time labels */ + ph_wait(500); + uint64_t p0 = phone_th->screen->stats.pixels; + ph_wait(1000); + uint64_t ticked = phone_th->screen->stats.pixels - p0; + CHECK(ticked > 0 && ticked < PHONE_W * PHONE_CONTENT_H / 10, + "one tick composited %llu pixels", (unsigned long long) ticked); + + /* Pause freezes the position */ + ph_tap(X_PLAY, CTL_Y); + CHECK(!app_music_test_playing(), "pause"); + int paused = app_music_test_elapsed(); + ph_wait(3000); + CHECK_EQ(app_music_test_elapsed(), paused, "paused position holds"); + ph_shot("music-paused"); + + /* Next and previous switch tracks and resume playback */ + ph_tap(X_NEXT, CTL_Y); + CHECK_EQ(app_music_test_track(), 3, "next track"); + CHECK(app_music_test_playing(), "skipping resumes playback"); + CHECK(app_music_test_elapsed() <= 3, "next starts at the top"); + ph_tap(X_PREV, CTL_Y); + CHECK_EQ(app_music_test_track(), 2, "previous track"); + ph_tap(X_PREV, CTL_Y); + ph_tap(X_PREV, CTL_Y); + ph_tap(X_PREV, CTL_Y); + CHECK_EQ(app_music_test_track(), PHONE_TRACK_COUNT - 1, + "previous wraps to the last track"); + ph_tap(X_NEXT, CTL_Y); + CHECK_EQ(app_music_test_track(), 0, "next wraps to the first track"); + ph_tap(X_NEXT, CTL_Y); + ph_tap(X_NEXT, CTL_Y); + CHECK_EQ(app_music_test_track(), 2, "back on the third track"); + + /* Dragging the slider to 3/4 seeks there */ + int x = SLIDER_X0 + (SLIDER_X1 - SLIDER_X0) * 3 / 4; + ph_drag(60, SLIDER_Y, x, SLIDER_Y); + int want = duration(2) * 3 / 4; + int got = app_music_test_elapsed(); + CHECK(got >= want && got <= want + 3, "seeked to %d s, want ~%d", got, + want); + CHECK_EQ(app_music_test_track(), 2, "seeking keeps the track"); + ph_shot("music-seeked"); + + /* Reaching the end rolls over to the next track */ + ph_drag(x, SLIDER_Y, SLIDER_X1 + 4, SLIDER_Y); + CHECK_EQ(app_music_test_track(), 3, "end of track advances"); + CHECK(app_music_test_elapsed() <= 3, "next track starts from the top"); + + /* Repeat keeps looping the same track */ + ph_tap(X_REPEAT, CTL_Y); + ph_drag(60, SLIDER_Y, SLIDER_X1 + 4, SLIDER_Y); + CHECK_EQ(app_music_test_track(), 3, "repeat loops the track"); + CHECK(app_music_test_elapsed() <= 3, "repeat restarts the track"); + ph_shot("music-repeat"); + ph_tap(X_REPEAT, CTL_Y); + + /* Shuffle picks some other track */ + ph_tap(X_SHUFFLE, CTL_Y); + ph_tap(X_NEXT, CTL_Y); + int shuffled = app_music_test_track(); + CHECK(shuffled != 3 && shuffled >= 0 && shuffled < PHONE_TRACK_COUNT, + "shuffle chose track %d", shuffled); + ph_shot("music-shuffle"); + ph_tap(X_SHUFFLE, CTL_Y); + + /* Theme switch repaints from state */ + int theme = phone_theme_index(); + ph_tap(X_PLAY, CTL_Y); + phone_theme_set(1); + ph_shot("music-now-playing-dark"); + phone_theme_set(theme); + ph_tap(X_PLAY, CTL_Y); + CHECK(app_music_test_playing(), "resumed"); +} + +void test_music(void) +{ + ph_open("Music"); + CHECK_EQ(phone_depth(), 1, "music list opened"); + CHECK_EQ(app_music_test_scroll(), 0, "list starts at the top"); + ph_shot("music-list"); + + test_now_playing(); + + /* Back on the list: the playing song is highlighted */ + phone_back(); + ph_wait(2000); + CHECK_EQ(phone_depth(), 1, "back to the list"); + ph_shot("music-list-playing"); + + /* Mini player toggles playback without leaving the list */ + ph_tap(PHONE_W - 24, MINI_Y + 24); + CHECK(!app_music_test_playing(), "mini player pauses"); + ph_tap(PHONE_W - 24, MINI_Y + 24); + CHECK(app_music_test_playing(), "mini player resumes"); + + /* Scrolling is not a tap, and rows still hit-test after it */ + ph_drag(120, 180, 120, 60); + CHECK_EQ(phone_depth(), 1, "scrolling opened nothing"); + int off = app_music_test_scroll(); + CHECK(off > 0, "list scrolled to %d", off); + ph_shot("music-list-scrolled"); + int last = PHONE_TRACK_COUNT - 1; + int y = LIST_TOP + last * ROW_H - off + ROW_H / 2; + CHECK(y > LIST_TOP && y < MINI_Y, "last row visible at %d", y); + ph_tap(120, y); + CHECK_EQ(phone_depth(), 2, "row tap after scrolling opens now playing"); + CHECK_EQ(app_music_test_track(), last, "tapped the last song"); + CHECK(app_music_test_playing(), "tapped song is playing"); + + /* The player outlives the app: playback continues while closed */ + ph_home(); + int e0 = app_music_test_elapsed(); + ph_wait(4000); + int e1 = app_music_test_elapsed(); + CHECK(e1 - e0 >= 3 && e1 - e0 <= 5, "background playback advanced %d s", + e1 - e0); + ph_open("Music"); + CHECK_EQ(app_music_test_track(), last, "reopened on the same song"); + CHECK(app_music_test_scroll() > 0, "list opens scrolled to the song"); + ph_shot("music-list-reopened"); + + /* Mini player opens now playing for the current song */ + ph_tap(100, MINI_Y + 24); + CHECK_EQ(phone_depth(), 2, "mini player opens now playing"); + ph_tap(X_PLAY, CTL_Y); + CHECK(!app_music_test_playing(), "paused before leaving"); + ph_home(); +} diff --git a/tests/phone-photos.c b/tests/phone-photos.c new file mode 100644 index 00000000..09178e12 --- /dev/null +++ b/tests/phone-photos.c @@ -0,0 +1,167 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * 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. + */ + +#include "phone-test.h" + +int app_photos_test_index(void); +bool app_photos_test_hires(void); +int app_photos_test_hires_count(void); +bool app_photos_test_grid_open(void); +int app_camera_test_shots(void); +int app_camera_test_flash(void); + +/* Thumbnail centers, content coordinates (grid at x 5, y 56; 74 + 4 gap) */ +#define THUMB_X(i) (5 + ((i) % 3) * 78 + 37) +#define THUMB_Y(i) (56 + ((i) / 3) * 78 + 30) + +void test_photos(void) +{ + ph_open("Photos"); + CHECK(app_photos_test_grid_open(), "photo grid on top"); + ph_shot("photos-grid"); + + /* Open the third photo; the full-size picture waits for the push */ + th_tap(phone_th, THUMB_X(2), CY(THUMB_Y(2))); + th_advance(phone_th, 120); + CHECK_EQ(phone_depth(), 2, "viewer pushed over the grid"); + CHECK_EQ(app_photos_test_index(), 2, "viewer opens on the tapped photo"); + CHECK(!app_photos_test_hires(), "full-size picture deferred"); + CHECK_EQ(app_photos_test_hires_count(), 0, "nothing decoded yet"); + ph_wait(400); + CHECK(app_photos_test_hires(), "full-size picture loaded after settling"); + CHECK_EQ(app_photos_test_hires_count(), 1, "one full-size picture"); + ph_shot("photos-viewer"); + + /* Swipe left: next photo, scaled thumbnail first, then full size */ + th_drag(phone_th, 200, CY(130), 40, CY(130), 8); + th_advance(phone_th, 240); + CHECK_EQ(app_photos_test_index(), 3, "swipe left shows the next photo"); + CHECK(!app_photos_test_hires(), "swipe re-arms the deferred load"); + CHECK(app_photos_test_hires_count() <= 1, "at most one full-size picture"); + ph_wait(400); + CHECK(app_photos_test_hires(), "next photo loaded at full size"); + CHECK_EQ(app_photos_test_hires_count(), 1, "previous picture freed"); + + /* Mid-drag frames must be consistent: blitted photo, repainted bars */ + th_press(phone_th, 200, CY(130)); + for (int x = 180; x >= 100; x -= 20) { + th_advance(phone_th, 16); + th_move(phone_th, x, CY(130)); + } + CHECK_EQ(th_damage_check(phone_th), 0, "mid-swipe frame consistent"); + th_release(phone_th, 100, CY(130)); + ph_wait(2000); + CHECK_EQ(app_photos_test_index(), 4, "slow long drag flips the photo"); + ph_drag(40, 130, 200, 130); + CHECK_EQ(app_photos_test_index(), 3, "and back"); + + /* Swipe right twice, then past the first photo (rubber band) */ + ph_drag(40, 130, 200, 130); + ph_drag(40, 130, 200, 130); + CHECK_EQ(app_photos_test_index(), 1, "swipe right goes back"); + ph_drag(40, 130, 200, 130); + CHECK_EQ(app_photos_test_index(), 0, "first photo"); + ph_drag(40, 130, 200, 130); + CHECK_EQ(app_photos_test_index(), 0, "no photo before the first"); + /* A short drag springs back */ + ph_drag(120, 130, 90, 130); + CHECK_EQ(app_photos_test_index(), 0, "short drag springs back"); + CHECK(app_photos_test_hires(), "settled photo at full size"); + CHECK_EQ(app_photos_test_hires_count(), 1, "still one picture"); + ph_shot("photos-viewer-first"); + + /* Tap hides the chrome, tap again restores it */ + ph_tap(120, 130); + ph_shot("photos-viewer-bare"); + ph_tap(120, 130); + + phone_back(); + ph_wait(2000); + CHECK_EQ(phone_depth(), 1, "back returns to the grid"); + CHECK(app_photos_test_grid_open(), "grid on top again"); + CHECK_EQ(app_photos_test_hires_count(), 0, "viewer freed its picture"); + CHECK_EQ(th_damage_check(phone_th), 0, "grid repainted after the pop"); + + ph_home(); + + /* Second cycle, closing the viewer before its deferred load: the first + * run warmed shared scratch buffers, so the heap must not grow now */ + size_t heap0 = th_heap_net(phone_th); + ph_open("Photos"); + th_tap(phone_th, THUMB_X(4), CY(THUMB_Y(4))); + th_advance(phone_th, 100); + CHECK_EQ(app_photos_test_index(), 4, "fifth photo opened"); + phone_back(); + ph_wait(2000); + CHECK_EQ(app_photos_test_hires_count(), 0, "deferred load cancelled"); + CHECK(app_photos_test_grid_open(), "grid after early back"); + ph_tap(THUMB_X(5), THUMB_Y(5)); + CHECK(app_photos_test_hires(), "last photo loaded"); + ph_drag(200, 130, 40, 130); + CHECK_EQ(app_photos_test_index(), 5, "no photo after the last"); + ph_home(); + CHECK(th_heap_net(phone_th) <= heap0, "photos freed everything (%zu > %zu)", + th_heap_net(phone_th), heap0); +} + +void test_camera(void) +{ + ph_open("Camera"); + CHECK_EQ(app_camera_test_shots(), 0, "no shots yet"); + ph_shot("camera"); + + /* Idle viewfinder: nothing repaints */ + uint32_t frames = phone_th->screen->stats.frames; + ph_wait(500); + CHECK_EQ(phone_th->screen->stats.frames, frames, "idle camera is static"); + + /* Shutter: flash fades out, counter goes up */ + th_tap(phone_th, PHONE_W / 2, CY(PHONE_CONTENT_H - 70 + 44)); + th_advance(phone_th, 48); + CHECK_EQ(app_camera_test_shots(), 1, "shutter takes a photo"); + int flash = app_camera_test_flash(); + CHECK(flash > 0 && flash < 255, "flash fading (%d)", flash); + ph_wait(500); + CHECK_EQ(app_camera_test_flash(), 0, "flash finished"); + frames = phone_th->screen->stats.frames; + ph_wait(500); + CHECK_EQ(phone_th->screen->stats.frames, frames, "static after the flash"); + + ph_tap(PHONE_W / 2, PHONE_CONTENT_H - 70 + 44); + CHECK_EQ(app_camera_test_shots(), 2, "second shot"); + ph_shot("camera-shot"); + + /* Video mode: shutter records instead of shooting */ + ph_tap(PHONE_W / 2 + 30, PHONE_CONTENT_H - 70 + 11); + ph_tap(PHONE_W / 2, PHONE_CONTENT_H - 70 + 44); + CHECK_EQ(app_camera_test_shots(), 2, "video mode does not count shots"); + ph_shot("camera-video"); + + /* Front camera, back to photo mode by swiping */ + ph_tap(PHONE_W - 38, PHONE_CONTENT_H - 70 + 44); + ph_drag(60, 100, 160, 100); + ph_tap(PHONE_W / 2, PHONE_CONTENT_H - 70 + 44); + CHECK_EQ(app_camera_test_shots(), 3, "photo mode again after swipe"); + ph_shot("camera-selfie"); + + ph_home(); + + /* The first run warms shared scratch paths; a second cycle must not + * grow the heap */ + size_t heap0 = th_heap_net(phone_th); + ph_open("Camera"); + ph_tap(PHONE_W - 38, PHONE_CONTENT_H - 70 + 44); + ph_tap(PHONE_W / 2, PHONE_CONTENT_H - 70 + 44); + ph_home(); + CHECK(th_heap_net(phone_th) <= heap0, "camera freed everything (%zu > %zu)", + th_heap_net(phone_th), heap0); +} diff --git a/tests/phone-regress.c b/tests/phone-regress.c new file mode 100644 index 00000000..ac88b778 --- /dev/null +++ b/tests/phone-regress.c @@ -0,0 +1,94 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Kit regressions that need no app on screen. + */ + +#include + +#include "phone-test.h" + +#define FIT_W 200 +#define FIT_H 24 + +static twin_pixmap_t *blank(void) +{ + twin_pixmap_t *px = twin_pixmap_create(TWIN_ARGB32, FIT_W, FIT_H); + twin_fill(px, 0xffffffff, TWIN_SOURCE, 0, 0, FIT_W, FIT_H); + return px; +} + +static bool same_pixels(twin_pixmap_t *a, twin_pixmap_t *b) +{ + for (int y = 0; y < FIT_H; y++) + if (memcmp(twin_pixmap_pointer(a, 0, y).v, + twin_pixmap_pointer(b, 0, y).v, FIT_W * 4)) + return false; + return true; +} + +/* + * What ui_text_fit() promises: the longest prefix of whole characters + * (trailing spaces dropped) that fits with "..." appended. + */ +static void expected_fit(int size, twin_coord_t room, const char *s, char *out) +{ + size_t best = 0; + for (size_t n = 1; s[n - 1]; n++) { + if (((unsigned char) s[n] & 0xc0) == 0x80) + continue; /* not a character boundary */ + size_t m = n; + while (m > 0 && s[m - 1] == ' ') + m--; + memcpy(out, s, m); + strcpy(out + m, "..."); + if (ui_text_width(size, out) <= room) + best = m; + } + memcpy(out, s, best); + strcpy(out + best, "..."); +} + +/* + * Truncation must not cut inside a UTF-8 sequence. A cut there used to + * end the measured string at the broken byte, so the stump "fit" and was + * drawn without its ellipsis. + */ +static void check_fit(const char *s, twin_coord_t box_w, const char *what) +{ + const int size = 14; + ui_rect_t r = ui_rect(0, 0, box_w, FIT_H); + CHECK(ui_text_width(size, s) > box_w - 8, "%s: needs truncating", what); + + char want[128]; + expected_fit(size, box_w - 8, s, want); + twin_pixmap_t *got = blank(), *ref = blank(); + ui_text_fit(got, size, r, UI_LEFT, 0xff000000, s); + ui_text_in(ref, size, r, UI_LEFT, 0xff000000, want); + CHECK(same_pixels(got, ref), "%s: drawn as \"%s\"", what, want); + twin_pixmap_destroy(got); + twin_pixmap_destroy(ref); +} + +static void test_text_fit_utf8(void) +{ + /* Accented letters are two-byte sequences, so many cut points fall + * mid-sequence. (The bundled Montserrat subset has no glyph for them: + * they measure zero, which does not matter to the cut.) */ + check_fit( + "Jos\xc3\xa9 Mar\xc3\xad" + "a Gonz\xc3\xa1lez Fern\xc3\xa1ndez", + 120, "accented name"); + for (twin_coord_t w = 40; w <= 120; w += 2) + check_fit("Andr\xc3\xa9 \xc3\x89milie Ren\xc3\xa9\xc3\xa9 Zo\xc3\xab", + w, "every width"); +} + +void test_regress(void) +{ + test_text_fit_utf8(); +} diff --git a/tests/phone-scenes.c b/tests/phone-scenes.c new file mode 100644 index 00000000..f3a72541 --- /dev/null +++ b/tests/phone-scenes.c @@ -0,0 +1,90 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Every app at every work-area height: the same app code must fill whatever + * client area the mobile policy gives it. Each view's client area is flooded + * with a sentinel colour, then repainted in full; any sentinel pixel left over + * is a row or column the layout never reached. + */ + +#include "phone-test.h" + +#define SENTINEL 0xffff00ff + +/* Pixel at pixmap coordinates, in the pixmap's own format */ +static uint32_t raw_pixel(const twin_pixmap_t *p, + twin_coord_t x, + twin_coord_t y) +{ + const uint8_t *row = (const uint8_t *) p->p.v + (size_t) y * p->stride; + if (p->format == TWIN_RGB16) + return ((const uint16_t *) row)[x]; + return ((const uint32_t *) row)[x]; +} + +/* Count client pixels still holding the sentinel after a full repaint */ +static int unpainted(ui_view_t *v) +{ + twin_window_t *w = v->window; + twin_pixmap_t *p = w->pixmap; + twin_rect_t c = w->client; + + /* twin_fill() works relative to the pixmap origin */ + twin_fill(p, SENTINEL, TWIN_SOURCE, c.left - p->origin_x, + c.top - p->origin_y, c.right - p->origin_x, + c.bottom - p->origin_y); + uint32_t mark = raw_pixel(p, c.left, c.top); + ui_view_invalidate_all(v); + ph_wait(500); + + int left = 0; + for (twin_coord_t y = c.top; y < c.bottom; y++) + for (twin_coord_t x = c.left; x < c.right; x++) + left += raw_pixel(p, x, y) == mark; + return left; +} + +/* Reserve @inset rows (an on-screen keyboard) under the open app @v and + * check it re-laid out to @want_h */ +static void check_height(ui_view_t *v, + const char *name, + twin_coord_t inset, + twin_coord_t want_h) +{ + twin_screen_set_inset(phone_screen(), inset); + ph_wait(500); + char what[64]; + snprintf(what, sizeof(what), "inset %d: %s view height", inset, name); + CHECK_EQ(ui_view_height(v), want_h, what); + int n = unpainted(v); + CHECK(n == 0, "inset %d: %s left %d client pixels unpainted", inset, name, + n); +} + +/* + * Open each app once and shrink the work area underneath it. Every change + * resizes the view, so this covers both laying out at creation and + * re-laying out in place. + */ +void test_scenes(void) +{ + for (int i = 0; i < phone_app_count(); i++) { + if (phone_app_at(i)->demo) + continue; /* test_demos() checks them with the inset too */ + const char *name = phone_app_at(i)->name; + ui_view_t *v = ph_open(name); + if (!v) + continue; + CHECK_EQ(ui_view_height(v), PHONE_CONTENT_H, name); + int n = unpainted(v); + CHECK(n == 0, "mobile: %s left %d client pixels unpainted", name, n); + check_height(v, name, 96, PHONE_CONTENT_H - 96); + check_height(v, name, 0, PHONE_CONTENT_H); + ph_check(name); + ph_home(); + } +} diff --git a/tests/phone-settings.c b/tests/phone-settings.c new file mode 100644 index 00000000..962c16eb --- /dev/null +++ b/tests/phone-settings.c @@ -0,0 +1,245 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Settings scenarios: every sub-screen opens and closes, theme, wallpaper, + * sliders and switches change state, and About refreshes only its values. + */ + +#include + +#include "phone-test.h" + +bool app_settings_item_rect(const char *label, ui_rect_t *out); +int app_settings_pref(const char *name); +int app_settings_about_refreshes(void); + +/* View coordinates of the top view to screen coordinates; works under any + * window manager (title bars, full-screen views) */ +static void to_screen(twin_coord_t *x, twin_coord_t *y) +{ + twin_window_t *w = phone_top()->window; + *x += w->pixmap->x + w->client.left; + *y += w->pixmap->y + w->client.top; +} + +static void view_tap(twin_coord_t x, twin_coord_t y) +{ + to_screen(&x, &y); + th_tap(phone_th, x, y); + ph_wait(2000); +} + +static void view_drag(twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1) +{ + to_screen(&x0, &y0); + to_screen(&x1, &y1); + th_drag(phone_th, x0, y0, x1, y1, 8); + ph_wait(2000); +} + +/* Find an item on the Settings screen on top, scrolling it into view */ +static bool find_item(const char *label, ui_rect_t *r) +{ + for (int tries = 0; tries < 4; tries++) { + if (!app_settings_item_rect(label, r)) + break; + twin_coord_t h = ui_view_height(phone_top()); + if (r->top >= PHONE_HEADER_H && r->bottom <= h) + return true; + twin_coord_t cx = ui_view_width(phone_top()) / 2; + if (r->bottom > h) + view_drag(cx, h - 20, cx, h - 120); + else + view_drag(cx, PHONE_HEADER_H + 20, cx, PHONE_HEADER_H + 120); + } + CHECK(0, "settings item '%s' reachable", label); + return false; +} + +static bool tap_item(const char *label) +{ + ui_rect_t r; + if (!find_item(label, &r)) + return false; + view_tap((r.left + r.right) / 2, (r.top + r.bottom) / 2); + return true; +} + +static bool top_is(const char *title) +{ + ui_view_t *v = phone_top(); + return v && v->title && !strcmp(v->title, title); +} + +/* Open a sub-screen from the root list and check it is on top */ +static bool open_sub(const char *label) +{ + if (!tap_item(label)) + return false; + CHECK(top_is(label), "%s opened", label); + CHECK_EQ(phone_depth(), 2, "sub-screen above the root list"); + return top_is(label); +} + +static void back_to_root(void) +{ + phone_back(); + ph_wait(2000); + CHECK(top_is("Settings"), "back on the Settings list"); + CHECK_EQ(phone_depth(), 1, "root list depth"); +} + +static void test_navigation(void) +{ + static const char *const subs[] = {"Display", "Sound", "Wallpaper", + "Date & time", "About"}; + static const char *const shots[] = {"settings-display", "settings-sound", + "settings-wallpaper", "settings-time", + "settings-about"}; + for (size_t i = 0; i < sizeof(subs) / sizeof(subs[0]); i++) { + if (!open_sub(subs[i])) + continue; + /* About shows live counters: consistency only, no golden */ + if (!strcmp(subs[i], "About")) + ph_check(shots[i]); + else + ph_shot(shots[i]); + if (i == 0) { + /* The nav bar's middle button is Back above home */ + th_tap(phone_th, PHONE_W / 2, PHONE_H - PHONE_NAV_H / 2); + ph_wait(2000); + CHECK(top_is("Settings"), "nav Back returns to Settings"); + } else { + back_to_root(); + } + } +} + +static void test_display(void) +{ + if (!open_sub("Display")) + return; + CHECK_EQ(phone_theme_index(), 0, "default theme"); + tap_item("Dark"); + CHECK_EQ(phone_theme_index(), 1, "Dark theme selected"); + ph_shot("settings-display-dark"); + + /* Brightness slider: drag the knob toward the left end */ + ui_rect_t r; + if (find_item("Brightness", &r)) { + int before = app_settings_pref("brightness"); + twin_coord_t cy = (r.top + r.bottom) / 2; + twin_coord_t x0 = r.left + 8 + (ui_w(r) - 16) * before / 1000; + view_drag(x0, cy, r.left + 8 + (ui_w(r) - 16) / 4, cy); + int after = app_settings_pref("brightness"); + CHECK(after < before && after >= 200 && after <= 300, + "brightness dragged from %d to %d", before, after); + CHECK(top_is("Display"), "slider drag does not navigate"); + } + + /* The theme reaches the home screen too */ + ph_home(); + ph_shot("settings-home-dark"); + + ph_open("Settings"); + if (open_sub("Display")) { + CHECK_EQ(app_settings_pref("brightness") < 700, 1, + "brightness kept across visits"); + tap_item("Sage"); + CHECK_EQ(phone_theme_index(), 0, "theme restored"); + } + back_to_root(); +} + +static void test_sound(void) +{ + if (!open_sub("Sound")) + return; + int vib = app_settings_pref("vibrate"); + tap_item("Vibrate"); + CHECK_EQ(app_settings_pref("vibrate"), !vib, "vibrate toggled"); + int keypad = app_settings_pref("keypad"); + tap_item("Keypad tones"); + CHECK_EQ(app_settings_pref("keypad"), !keypad, "keypad tones toggled"); + + ui_rect_t r; + if (find_item("Volume", &r)) { + twin_coord_t cy = (r.top + r.bottom) / 2; + view_drag(r.left + 8 + (ui_w(r) - 16) / 2, cy, r.right - 2, cy); + CHECK_EQ(app_settings_pref("volume"), 1000, "volume dragged to max"); + } + ph_shot("settings-sound-changed"); + CHECK_EQ(th_damage_check(phone_th), 0, "sound screen consistent"); + + /* Restore the switches; the slider value is kept for the next check */ + tap_item("Vibrate"); + tap_item("Keypad tones"); + CHECK_EQ(app_settings_pref("vibrate"), vib, "vibrate restored"); + CHECK_EQ(app_settings_pref("keypad"), keypad, "keypad restored"); + back_to_root(); + if (open_sub("Sound")) { + CHECK_EQ(app_settings_pref("volume"), 1000, "volume kept"); + back_to_root(); + } +} + +static void test_wallpaper(void) +{ + if (!open_sub("Wallpaper")) + return; + CHECK_EQ(phone_wallpaper(), 0, "default wallpaper"); + tap_item("Dusk"); + CHECK_EQ(phone_wallpaper(), 1, "Dusk selected"); + ph_shot("settings-wallpaper-dusk"); + tap_item(phone_wallpaper_name(0)); + CHECK_EQ(phone_wallpaper(), 0, "wallpaper restored"); + back_to_root(); +} + +static void test_about(void) +{ + if (!open_sub("About")) + return; + /* Bring the live values into view */ + ui_rect_t r; + find_item("Pixels", &r); + ph_wait(1100); + + twin_screen_t *screen = phone_screen(); + int ticks = app_settings_about_refreshes(); + uint64_t pixels = screen->stats.pixels; + uint32_t frames = screen->stats.frames; + ph_wait(1100); + uint64_t delta = screen->stats.pixels - pixels; + CHECK(app_settings_about_refreshes() > ticks, "about values refreshed"); + CHECK(screen->stats.frames > frames, "refresh reached the screen"); + CHECK(delta > 0 && delta < (uint64_t) PHONE_W * PHONE_H / 4, + "refresh repainted %llu px, full frame is %d", + (unsigned long long) delta, PHONE_W * PHONE_H); + ph_check("settings-about-stats"); + back_to_root(); +} + +void test_settings(void) +{ + ph_open("Settings"); + CHECK(top_is("Settings"), "settings root on top"); + ph_shot("settings"); + + test_navigation(); + test_display(); + test_sound(); + test_wallpaper(); + test_about(); + + ph_home(); + CHECK_EQ(phone_theme_index(), 0, "theme 0 at exit"); + CHECK_EQ(phone_wallpaper(), 0, "wallpaper 0 at exit"); +} diff --git a/tests/phone-sms.c b/tests/phone-sms.c new file mode 100644 index 00000000..7f47d9b8 --- /dev/null +++ b/tests/phone-sms.c @@ -0,0 +1,126 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Messages: thread list, chat scrolling, composer and soft keyboard. + */ + +#include + +#include "data.h" +#include "phone-test.h" + +int app_sms_test_offset(void); +int app_sms_test_max_offset(void); +int app_sms_test_messages(void); +int app_sms_test_keyboard(void); +const char *app_sms_test_field(void); +const char *app_sms_test_message(int i); + +/* Layout mirrored from sms.c (content coordinates) */ +#define ROW_H 52 +#define COMPOSER_H 36 + +static twin_coord_t composer_cy(bool kbd) +{ + return PHONE_CONTENT_H - COMPOSER_H / 2 - (kbd ? UI_KBD_H : 0); +} + +#define SEND_X (PHONE_W - 6 - 20) +#define FIELD_X 90 + +void test_sms(void) +{ + const phone_sms_thread_t *threads = phone_sms_threads(); + + ph_open("Messages"); + CHECK_EQ(phone_depth(), 1, "thread list open"); + CHECK_EQ(app_sms_test_offset(), -1, "list is not a chat"); + ph_shot("sms-list"); + + /* Fourth thread: the longest conversation, so it scrolls */ + ph_tap(120, PHONE_HEADER_H + 3 * ROW_H + ROW_H / 2); + CHECK_EQ(phone_depth(), 2, "chat pushed over the list"); + ui_view_t *top = phone_top(); + CHECK_STR(top ? top->title : NULL, threads[3].contact_name, "chat title"); + int stock = app_sms_test_messages(); + CHECK_EQ(stock, (int) threads[3].msg_count, "stock messages shown"); + int max = app_sms_test_max_offset(); + CHECK(max > 0, "conversation overflows (max %d)", max); + CHECK_EQ(app_sms_test_offset(), max, "opens at the newest message"); + CHECK_EQ(app_sms_test_keyboard(), 0, "keyboard hidden on open"); + ph_shot("sms-chat"); + + /* Dragging down reveals older messages, and is not a tap */ + ph_drag(120, 60, 120, 160); + CHECK(app_sms_test_offset() < max, "dragged to %d", app_sms_test_offset()); + CHECK_EQ(app_sms_test_keyboard(), 0, "drag does not open the keyboard"); + + /* Send is disabled while the field is empty */ + ph_tap(SEND_X, composer_cy(false)); + CHECK_EQ(app_sms_test_messages(), stock, "empty send ignored"); + + /* Field tap raises the keyboard and re-anchors at the newest message */ + ph_tap(FIELD_X, composer_cy(false)); + CHECK_EQ(app_sms_test_keyboard(), 1, "field tap shows the keyboard"); + CHECK_EQ(app_sms_test_offset(), app_sms_test_max_offset(), + "latest message visible above the composer"); + ph_tap(SEND_X, composer_cy(true)); + CHECK_EQ(app_sms_test_messages(), stock, + "empty send ignored with keyboard"); + + ph_key('h'); + ph_key('i'); + const char *f = app_sms_test_field(); + CHECK_STR(f, "hi", "typed on the soft keyboard"); + ph_shot("sms-typing"); + + ph_tap(SEND_X, composer_cy(true)); + CHECK_EQ(app_sms_test_messages(), stock + 1, "send appends a message"); + const char *m = app_sms_test_message(stock); + CHECK_STR(m, "hi", "message sent"); + f = app_sms_test_field(); + CHECK(f && !f[0], "field cleared after send"); + CHECK_EQ(app_sms_test_keyboard(), 1, "keyboard stays open after send"); + CHECK_EQ(app_sms_test_offset(), app_sms_test_max_offset(), + "scrolled to the new message"); + ph_shot("sms-sent"); + + /* Hardware keys type into the field; Enter sends */ + th_key(phone_th, 'o'); + th_key(phone_th, 'k'); + ph_wait(100); + f = app_sms_test_field(); + CHECK_STR(f, "ok", "typed on the hardware keyboard"); + th_key(phone_th, '\r'); + ph_wait(500); + CHECK_EQ(app_sms_test_messages(), stock + 2, "Enter sends"); + + /* A drag in the shrunken chat area scrolls without dismissing */ + ph_drag(120, PHONE_HEADER_H + 5, 120, PHONE_HEADER_H + 55); + CHECK_EQ(app_sms_test_keyboard(), 1, "scroll keeps the keyboard"); + + /* Tapping the conversation outside the composer dismisses it */ + ph_tap(120, PHONE_HEADER_H + 20); + CHECK_EQ(app_sms_test_keyboard(), 0, "outside tap hides the keyboard"); + CHECK_EQ(app_sms_test_offset(), app_sms_test_max_offset(), + "newest message stays in view after the reflow"); + + /* Theme switch repaints from state */ + phone_theme_set(1); + ph_shot("sms-chat-dark"); + phone_theme_set(0); + ph_wait(100); + + /* Nav bar Back pops to the list */ + th_tap(phone_th, PHONE_W / 2, PHONE_H - PHONE_NAV_H / 2); + ph_wait(2000); + CHECK_EQ(phone_depth(), 1, "Back returns to the list"); + top = phone_top(); + CHECK_STR(top ? top->title : NULL, "Messages", "list on top"); + + ph_home(); +} diff --git a/tests/phone-test.h b/tests/phone-test.h new file mode 100644 index 00000000..da10a903 --- /dev/null +++ b/tests/phone-test.h @@ -0,0 +1,66 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Shared helpers for the cellphone scenario suite. Each app contributes + * tests/phone-.c defining test_(); test-cellphone.c runs them in + * a fixed order on one booted, unlocked phone sitting on the home screen. + */ + +#ifndef _TWIN_PHONE_TEST_H_ +#define _TWIN_PHONE_TEST_H_ + +#include "harness.h" +#include "phone.h" + +extern th_t *phone_th; + +/* Screen y of content y (content starts below the status bar) */ +#define CY(y) ((twin_coord_t) ((y) + PHONE_STATUS_H)) + +/* Settle, check damage consistency, compare the golden CRC */ +void ph_shot(const char *name); + +/* Like ph_shot() for screens showing live counters: no golden CRC */ +void ph_check(const char *name); + +/* Open an app by registry name and settle; checks it is on top */ +ui_view_t *ph_open(const char *name); + +/* Return to the home screen and settle; checks the stack is empty */ +void ph_home(void); + +/* Tap at content coordinates and settle */ +void ph_tap(twin_coord_t x, twin_coord_t y); + +/* Drag at content coordinates and settle */ +void ph_drag(twin_coord_t x0, + twin_coord_t y0, + twin_coord_t x1, + twin_coord_t y1); + +/* Tap the soft-keyboard key at its position in the letters layout, else in + * the digits/symbols one (the keyboard must be showing that layout) */ +void ph_key(int code); + +/* Settle animations and timers for @ms of virtual time */ +void ph_wait(int ms); + +/* App suites */ +void test_dialer(void); +void test_calllog(void); +void test_contacts(void); +void test_sms(void); +void test_music(void); +void test_photos(void); +void test_camera(void); +void test_settings(void); +void test_games(void); +void test_scenes(void); +void test_demos(void); +void test_regress(void); + +#endif /* _TWIN_PHONE_TEST_H_ */ diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c new file mode 100644 index 00000000..abe26e8c --- /dev/null +++ b/tests/test-cellphone.c @@ -0,0 +1,284 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Cellphone scenario test: boots the phone headless with a fixed clock and + * drives the user journey (unlock, every app, navigation, swipe-back, + * paging, keyboard input, theme switch, apps opened from the launcher). + * + * Gates: + * - structure: stack depth, top view, focus after each step + * - pixels: golden CRCs of key screens (skipped if build options differ) + * - consistency: incremental scan-out equals a full recomposite + * - memory: heap returns to baseline after app cycles; peak bounded + * - work: cache hit rate and composited pixels per transition frame + */ + +#include +#include + +#include "phone-test.h" + +static void fixed_clock(phone_clock_t *c) +{ + *c = (phone_clock_t) { + .hour = 12, .min = 3, .sec = 0, .wday = 6, .mday = 9, .mon = 4}; +} + +const char *app_calc_eval(const char *typed); + +th_t *phone_th; +static th_t *t; + +void ph_wait(int ms) +{ + th_settle(t, ms); +} + +void ph_check(const char *name) +{ + th_settle(t, 2000); + CHECK_EQ(th_damage_check(t), 0, name); +} + +void ph_shot(const char *name) +{ + ph_check(name); + th_golden("cellphone", name, th_crc(t), t); +} + +ui_view_t *ph_open(const char *name) +{ + int app = phone_app_find(name); + CHECK(app >= 0, "app %s registered", name); + phone_open(app); + th_settle(t, 2000); + ui_view_t *top = phone_top(); + CHECK(top && top->app == app, "%s on top", name); + return top; +} + +void ph_home(void) +{ + phone_home(); + th_settle(t, 2000); + CHECK_EQ(phone_depth(), 0, "back on the home screen"); +} + +void ph_tap(twin_coord_t x, twin_coord_t y) +{ + th_tap(t, x, CY(y)); + th_settle(t, 2000); +} + +void ph_drag(twin_coord_t x0, twin_coord_t y0, twin_coord_t x1, twin_coord_t y1) +{ + th_drag(t, x0, CY(y0), x1, CY(y1), 8); + th_settle(t, 2000); +} + +void ph_key(int code) +{ + /* Letters first, then the digits/symbols layout */ + ui_rect_t r, area = ui_kbd_rect(PHONE_CONTENT_H); + for (int symbols = 0; symbols < 2; symbols++) { + ui_kbd_t k; + ui_kbd_init(&k); + k.symbols = symbols; + if (ui_kbd_find(&k, area, code, &r)) { + ph_tap((r.left + r.right) / 2, (r.top + r.bottom) / 2); + return; + } + } + CHECK(0, "no key for '%c'", code); +} + +static void unlock(void) +{ + /* Drag the knob all the way along the track */ + th_drag(t, 33, 295, 230, 295, 10); + th_settle(t, 2000); +} + +static void test_boot_and_unlock(void) +{ + CHECK(phone_locked(), "boots locked"); + ph_shot("lock"); + CHECK(t->screen->active && t->screen->active->window && + t->screen->active->window->style == TwinWindowFullscreen, + "lock screen focused"); + + /* A short drag springs back */ + th_drag(t, 33, 295, 90, 295, 4); + th_settle(t, 2000); + CHECK(phone_locked(), "short slide stays locked"); + + unlock(); + CHECK(!phone_locked(), "full slide unlocks"); + CHECK_EQ(phone_depth(), 0, "home after unlock"); + ph_shot("home"); +} + +static void test_wrap(void) +{ + twin_coord_t widest = 0; + int n = ui_text_wrap(NULL, FONT_SM, 0, 0, 100, 14, 0, + "Hey, you coming to " + "the meeting?", + &widest); + CHECK(n >= 2 && n <= 4, "wrapped into %d lines", n); + CHECK(widest <= 100 && widest > 40, "widest line %d", widest); + CHECK_EQ(ui_text_wrap(NULL, FONT_SM, 0, 0, 200, 14, 0, "short", NULL), 1, + "short text is one line"); + CHECK(ui_text_wrap(NULL, FONT_SM, 0, 0, 30, 14, 0, "Supercalifragilistic", + NULL) >= 1, + "overlong word still terminates"); +} + +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); + CHECK_EQ(phone_depth(), 1, "calculator opened"); + ph_shot("calculator"); + + CHECK_STR(app_calc_eval("C12+30="), "42", "12+30"); + 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"); + /* Ten 12-digit terms overflow the 64-bit fixed-point accumulator */ + char big[160]; + for (int sign = 0; sign < 2; sign++) { + strcpy(big, "C0"); + for (int i = 0; i < 10; i++) + strcat(big, sign ? "-999999999999" : "+999999999999"); + strcat(big, "="); + CHECK_STR(app_calc_eval(big), "Error", + sign ? "subtraction overflow" : "addition overflow"); + } + CHECK_STR(app_calc_eval("C0-999999999999="), "-999999999999", + "large negative still fits"); + /* Typed keys: U+0131 would narrow to '1' */ + app_calc_eval("C"); + th_key(t, '7'); + th_key(t, 0x131); + th_settle(t, 100); + CHECK_STR(app_calc_eval(""), "7", "non-ASCII key ignored"); + th_settle(t, 500); + CHECK_EQ(th_damage_check(t), 0, "calculator display consistent"); + + phone_back(); + th_settle(t, 2000); + CHECK_EQ(phone_depth(), 0, "back to home"); +} + +/* + * Budgets for the whole phone: memory returns to baseline after visiting + * every phone app, the peak stays bounded, transitions cost about one frame of + * compositing per animation frame, and a warm pass is served by the cache. + */ +static void test_budgets(void) +{ + ph_home(); + /* One warm-up pass so caches and reuse pools reach steady state */ + for (int i = 0; i < phone_app_count(); i++) { + if (phone_app_at(i)->demo) + continue; /* own windows, covered by test_demos() */ + phone_open(i); + th_settle(t, 2000); + ph_home(); + } + size_t base = th_heap_net(t); + /* The allocator's peak counts every pool still held from earlier tests, + * so measure its growth from the whole heap, not from the net figure */ + size_t raw_base = th_heap_raw(); + twin_memory_reset_peak(); + twin_cache_stats_t c0, c1; + twin_cache_get_stats(&c0); + + uint32_t worst_frame = 0; + for (int i = 0; i < phone_app_count(); i++) { + if (phone_app_at(i)->demo) + continue; + twin_screen_stats_t s0 = t->screen->stats; + phone_open(i); + th_settle(t, 2000); + twin_screen_stats_t s1 = t->screen->stats; + uint32_t frames = s1.frames - s0.frames; + uint32_t per = + frames ? (uint32_t) ((s1.pixels - s0.pixels) / frames) : 0; + if (per > worst_frame) + worst_frame = per; + CHECK_EQ(phone_depth(), 1, phone_app_at(i)->name); + ph_home(); + } + twin_cache_get_stats(&c1); + size_t after = th_heap_net(t); + twin_memory_info_t mem; + twin_memory_get_info(&mem); + + uint32_t hits = c1.hits - c0.hits, misses = c1.misses - c0.misses; + uint32_t rate = hits + misses ? hits * 100 / (hits + misses) : 100; + /* Peak on the same footing as base: the growth over the whole heap, + * added to the net baseline, so pools other tests left behind do not + * count */ + size_t peak = base + (mem.peak_bytes - raw_base); + printf("cellphone: heap at home %zu bytes, peak over all apps %zu bytes\n", + base, peak); + printf("cellphone: worst opening animation %u pixels/frame (screen %d)\n", + worst_frame, PHONE_W * PHONE_H); + printf( + "cellphone: warm cache hit rate %u%% (%u hits, %u misses, %zu " + "bytes resident)\n", + rate, hits, misses, c1.bytes); + + CHECK(after <= base, "visiting every app leaks (%zu -> %zu)", base, after); + /* Home + one app + bars + caches, not one surface per app */ + size_t surface = (size_t) PHONE_W * PHONE_CONTENT_H * 4; + CHECK(peak < base + 3 * surface, "peak bounded (%zu over a %zu baseline)", + peak, base); + CHECK(worst_frame <= (uint32_t) (PHONE_W * PHONE_H) * 3 / 2, + "transition frames composite about one screen (%u)", worst_frame); + CHECK(rate >= 80, "warm pass mostly served by the cache (%u%%)", rate); +} + +int main(void) +{ + phone_set_clock_source(fixed_clock); + t = phone_th = th_create(PHONE_W, PHONE_H); + phone_boot(t->screen); + th_settle(t, 2000); + + test_boot_and_unlock(); + test_wrap(); + test_calculator(); + + /* App suites; each starts and ends on the home screen */ + void (*suites[])(void) = {test_dialer, test_calllog, test_contacts, + test_sms, test_music, test_photos, + test_camera, test_settings, test_games, + test_scenes, test_demos, test_regress}; + for (size_t i = 0; i < sizeof(suites) / sizeof(suites[0]); i++) { + suites[i](); + if (phone_depth()) + ph_home(); + } + test_budgets(); + + /* Shutdown closes what the phone opened, demo windows included */ + for (int i = 0; i < phone_app_count(); i++) + if (phone_app_at(i)->demo) { + phone_open(i); + th_settle(t, 2000); + break; + } + phone_shutdown(); + CHECK(!t->screen->windows, "shutdown leaves no window behind"); + th_destroy(t); + twin_cache_flush(); + return th_report("cellphone"); +} diff --git a/tests/test-core.c b/tests/test-core.c new file mode 100644 index 00000000..5b615330 --- /dev/null +++ b/tests/test-core.c @@ -0,0 +1,1475 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Core rendering tests: compositor, coverage cache, text, gradients, + * tweens, and object lifetimes. Optimizations are checked differentially: + * the optimized path must produce exactly the bytes of the plain one. + */ + +#include +#include +#include + +#include "harness.h" + +#define W 160 +#define H 120 + +static twin_pixmap_t *solid(twin_format_t fmt, + twin_coord_t w, + twin_coord_t h, + twin_argb32_t color) +{ + twin_pixmap_t *p = twin_pixmap_create(fmt, w, h); + twin_fill(p, color, TWIN_SOURCE, 0, 0, w, h); + return p; +} + +static void test_damage_merge(void) +{ + th_t *t = th_create(W, H); + twin_screen_t *s = t->screen; + + twin_screen_damage(s, 0, 0, 10, 10); + twin_screen_damage(s, 100, 100, 110, 110); + CHECK_EQ(s->n_damage, 2, "disjoint rectangles stay apart"); + + twin_screen_damage(s, 10, 0, 20, 10); /* edge-adjacent: free merge */ + CHECK_EQ(s->n_damage, 2, "adjacent rectangle merges"); + twin_screen_damage(s, 2, 2, 8, 8); /* contained */ + CHECK_EQ(s->n_damage, 2, "contained rectangle absorbed"); + + bool merged = false; + for (int i = 0; i < s->n_damage; i++) + if (s->damage[i].left == 0 && s->damage[i].right == 20) + merged = true; + CHECK(merged, "adjacent rectangles become one 20x10 rectangle"); + + /* Overflow folds into the cheapest neighbor, never drops damage */ + for (int i = 0; i < 20; i++) + twin_screen_damage(s, i * 7, (i * 13) % 100, i * 7 + 3, + (i * 13) % 100 + 3); + CHECK(s->n_damage <= TWIN_DAMAGE_RECTS, "damage list bounded"); + twin_screen_update(s); + CHECK_EQ(s->n_damage, 0, "update clears damage"); + th_destroy(t); +} + +/* A scene that exercises every compositor path */ +static void build_scene(th_t *t, twin_pixmap_t **out, int *n) +{ + twin_screen_t *s = t->screen; + int k = 0; + twin_pixmap_t *bg = solid(TWIN_RGB16, W, H, 0xff336699); /* opaque */ + twin_pixmap_show(bg, s, NULL); + out[k++] = bg; + + twin_pixmap_t *card = solid(TWIN_ARGB32, 80, 60, 0xffeeeeee); + card->opaque_rect = (twin_rect_t) {0, 80, 0, 60}; + twin_pixmap_move(card, 20, 20); + twin_pixmap_set_shadow(card, 3, 4, 8, 0x90); + twin_pixmap_show(card, s, bg); + out[k++] = card; + + twin_pixmap_t *glass = solid(TWIN_ARGB32, 60, 60, 0x80800000); /* 50% */ + twin_pixmap_move(glass, 70, 40); + twin_pixmap_show(glass, s, card); + out[k++] = glass; + + twin_pixmap_t *faded = solid(TWIN_RGB16, 40, 40, 0xff00ff00); + twin_pixmap_move(faded, 110, 10); + twin_pixmap_set_opacity(faded, 0x60); + twin_pixmap_show(faded, s, glass); + out[k++] = faded; + + twin_pixmap_t *top = solid(TWIN_ARGB32, 30, 100, 0xff202020); + top->opaque_rect = (twin_rect_t) {0, 30, 0, 100}; + twin_pixmap_move(top, 5, 10); + twin_pixmap_set_shadow(top, -2, 0, 5, 0xff); + twin_pixmap_show(top, s, faded); + out[k++] = top; + *n = k; +} + +static void test_occlusion_differential(void) +{ + th_t *t = th_create(W, H); + twin_pixmap_t *p[8]; + int n; + build_scene(t, p, &n); + + twin_screen_damage(t->screen, 0, 0, W, H); + t->screen->stats = (twin_screen_stats_t) {0}; + twin_screen_update(t->screen); + uint32_t fast = th_crc(t); + twin_screen_stats_t on = t->screen->stats; + + _twin_debug |= TWIN_DEBUG_NO_OCCLUSION; + twin_screen_damage(t->screen, 0, 0, W, H); + t->screen->stats = (twin_screen_stats_t) {0}; + twin_screen_update(t->screen); + _twin_debug &= ~TWIN_DEBUG_NO_OCCLUSION; + uint32_t slow = th_crc(t); + twin_screen_stats_t off = t->screen->stats; + + CHECK(fast == slow, "occlusion culling changed pixels: %08x vs %08x", fast, + slow); + CHECK(on.occluded_pixels > 0, "opaque layers occluded something"); + CHECK(on.layer_pixels < off.layer_pixels, + "culling composites fewer pixels (%llu vs %llu)", + (unsigned long long) on.layer_pixels, + (unsigned long long) off.layer_pixels); + th_golden("core", "scene", fast, t); + + /* Partial damage must agree with the full frame */ + twin_pixmap_move(p[2], 75, 45); + twin_pixmap_set_opacity(p[3], 0xc0); + twin_pixmap_set_shadow(p[1], 6, 6, 12, 0x60); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "stale pixels after moves"); + + for (int i = n - 1; i >= 0; i--) + twin_pixmap_destroy(p[i]); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "stale pixels after teardown"); + th_destroy(t); +} + +/* Full frame with extra debug switches; flags and stats are restored */ +static uint32_t full_frame(th_t *t, unsigned debug) +{ + unsigned saved = _twin_debug; + twin_screen_stats_t stats = t->screen->stats; + _twin_debug |= debug; + twin_screen_damage(t->screen, 0, 0, W, H); + twin_screen_update(t->screen); + _twin_debug = saved; + t->screen->stats = stats; + return th_crc(t); +} + +/* + * Rows with more layers than the culled path can track fall back to plain + * bottom-up compositing. Both paths must give the same pixels: compare + * them on the shared scene, then on a stack deep enough to overflow. + */ +static void test_row_fallback(void) +{ + th_t *t = th_create(W, H); + twin_pixmap_t *p[8]; + int n; + build_scene(t, p, &n); + uint32_t culled = full_frame(t, 0); + CHECK(full_frame(t, TWIN_DEBUG_ROW_FALLBACK) == culled, + "fallback path matches the culled path"); + + enum { DEEP = 40 }; + twin_pixmap_t *deep[DEEP]; + twin_pixmap_t *below = p[n - 1]; + for (int i = 0; i < DEEP; i++) { + bool opaque = i % 5 == 0; + deep[i] = solid(i % 2 ? TWIN_RGB16 : TWIN_ARGB32, 60, 50, + opaque ? 0xff000000 | (0x050403u * i) + : 0x40000000 | (0x030507u * i)); + if (opaque) + deep[i]->opaque_rect = (twin_rect_t) {0, 60, 0, 50}; + if (i % 7 == 3) + twin_pixmap_set_shadow(deep[i], 2, 2, 4, 0x80); + if (i % 6 == 1) + twin_pixmap_set_opacity(deep[i], 0xa0); + twin_pixmap_move(deep[i], 10 + i, 30 + (i % 4)); /* all overlap */ + twin_pixmap_show(deep[i], t->screen, below); + below = deep[i]; + } + uint32_t natural = full_frame(t, 0); + CHECK(full_frame(t, TWIN_DEBUG_ROW_FALLBACK) == natural, + "deep stack: overflowing rows match the forced fallback"); + CHECK_EQ(th_damage_check(t), 0, "deep stack: culling off agrees"); + twin_pixmap_move(deep[DEEP / 2], 90, 80); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "deep stack: partial damage consistent"); + + for (int i = DEEP - 1; i >= 0; i--) + twin_pixmap_destroy(deep[i]); + for (int i = n - 1; i >= 0; i--) + twin_pixmap_destroy(p[i]); + th_destroy(t); +} + +#if defined(CONFIG_DROP_SHADOW) +static void test_shadow(void) +{ + th_t *t = th_create(W, H); + /* White whatever the window manager paints behind everything */ + t->screen->background_color = 0xffffffff; + twin_screen_damage(t->screen, 0, 0, W, H); + twin_pixmap_t *p = solid(TWIN_ARGB32, 40, 40, 0xff000080); + twin_pixmap_move(p, 40, 40); + twin_pixmap_set_shadow(p, 4, 4, 6, 0xc0); + twin_pixmap_show(p, t->screen, NULL); + th_frame(t); + + twin_argb32_t far = th_pixel(t, 5, 5); + twin_argb32_t under = th_pixel(t, 82, 82); /* inside shadow, past edge */ + twin_argb32_t edge = th_pixel(t, 90, 60); /* outer falloff */ + CHECK(far == 0xffffffff, "background untouched far away: %08x", far); + CHECK((under & 0xff) < 0xc0, "shadow darkens next to the box: %08x", under); + CHECK((under >> 24) == 0xff, "shadow keeps the frame opaque: %08x", under); + CHECK((edge & 0xff) > (under & 0xff), "shadow fades outwards"); + + /* Shadows move with their pixmap and leave nothing behind */ + twin_pixmap_move(p, 10, 10); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "shadow damage covers its extent"); + CHECK(th_pixel(t, 82, 82) == 0xffffffff, "old shadow erased"); + + twin_pixmap_set_shadow(p, 0, 0, 0, 0); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "removing shadow damages it"); + twin_pixmap_destroy(p); + th_destroy(t); +} +#endif + +static void test_rgb16_and_opacity(void) +{ + th_t *t = th_create(W, H); + t->screen->background_color = 0xff000000; + twin_pixmap_t *p = solid(TWIN_RGB16, 20, 20, 0xffff0000); + twin_pixmap_show(p, t->screen, NULL); + th_frame(t); + CHECK(th_pixel(t, 5, 5) == 0xffff0000, "RGB16 red scans out red: %08x", + th_pixel(t, 5, 5)); + CHECK(th_pixel(t, 19, 19) == 0xffff0000, "every RGB16 pixel converted"); + + twin_pixmap_set_opacity(p, 0x80); + th_frame(t); + twin_argb32_t v = th_pixel(t, 5, 5); + CHECK(((v >> 16) & 0xff) > 0x70 && ((v >> 16) & 0xff) < 0x90, + "half opacity over black is half red: %08x", v); + twin_pixmap_destroy(p); + th_destroy(t); +} + +static void test_stub(void) +{ + twin_pixmap_t *s = _twin_pixmap_create_stub(TWIN_ARGB32, 50, 50); + twin_path_t *path = twin_path_create(); + twin_path_circle(path, 25 << 16, 25 << 16, 20 << 16); + twin_paint_path(s, 0xffff0000, path); + twin_fill(s, 0xff00ff00, TWIN_SOURCE, 0, 0, 50, 50); + twin_text_draw(s, NULL, 12 << 16, 0, 20 << 16, 0xff000000, "stub"); + twin_pixmap_reset_clip(s); + CHECK(s->clip.right == 0, "stub clip stays empty after reset"); + twin_path_destroy(path); + twin_pixmap_destroy(s); +} + +/* Draw a mix of shapes and text; used for cache equivalence */ +static void draw_widgets(twin_pixmap_t *p, twin_coord_t ox, twin_coord_t oy) +{ + twin_path_t *path = twin_path_create(); + twin_fill(p, 0xffffffff, TWIN_SOURCE, 0, 0, p->width, p->height); + for (int i = 0; i < 3; i++) { + twin_path_empty(path); + twin_path_rounded_rectangle( + path, twin_int_to_fixed(ox + 4 + i * 40), twin_int_to_fixed(oy + 4), + twin_int_to_fixed(36), twin_int_to_fixed(36), twin_int_to_fixed(9), + twin_int_to_fixed(9)); + twin_paint_path(p, 0xff3080c0, path); + } + twin_path_empty(path); + twin_path_circle(path, twin_int_to_fixed(ox + 60), + twin_int_to_fixed(oy + 70), twin_int_to_fixed(14)); + twin_paint_path_linear(p, path, 0, twin_int_to_fixed(oy + 56), 0xffff8000, + 0, twin_int_to_fixed(oy + 84), 0xff800000); +#if defined(CONFIG_FONT_MONTSERRAT) + twin_text_draw(p, &twin_font_montserrat, twin_int_to_fixed(14), + twin_int_to_fixed(ox + 4), twin_int_to_fixed(oy + 100), + 0xff202020, "Hello, Mado 0123"); +#endif + twin_text_draw(p, NULL, twin_int_to_fixed(12), twin_int_to_fixed(ox + 90), + twin_int_to_fixed(oy + 100), 0xff202020, "Roman"); + twin_path_destroy(path); +} + +/* Independent of CONFIG_CACHE_SIZE, which may be 0 */ +#define TEST_BUDGET (24 * 1024) + +static void test_cache_equivalence(void) +{ + twin_pixmap_t *a = twin_pixmap_create(TWIN_ARGB32, W, H); + twin_pixmap_t *b = twin_pixmap_create(TWIN_ARGB32, W, H); + size_t bytes = (size_t) a->stride * H; + twin_cache_stats_t st0, st1, st2; + + twin_cache_set_budget(0); + draw_widgets(a, 0, 0); + twin_cache_set_budget(TEST_BUDGET); + twin_cache_flush(); + twin_cache_get_stats(&st0); + draw_widgets(b, 0, 0); /* cold: rasterize and insert */ + twin_cache_get_stats(&st1); + CHECK(!memcmp(a->p.v, b->p.v, bytes), "cold cache changes pixels"); + draw_widgets(b, 0, 0); /* warm: every mask from the cache */ + twin_cache_get_stats(&st2); + CHECK(!memcmp(a->p.v, b->p.v, bytes), "warm cache changes pixels"); + CHECK(st1.misses > st0.misses, "cold pass misses"); + CHECK(st2.misses == st1.misses, + "warm pass rasterizes nothing (%u new misses)", + st2.misses - st1.misses); + CHECK(st2.hits > st1.hits, "warm pass hits"); + CHECK(st2.bytes <= st2.budget, "cache within budget"); + + /* Translation invariance: the same shapes 7 px right, 3 px down */ + twin_cache_set_budget(0); + draw_widgets(a, 7, 3); + twin_cache_set_budget(TEST_BUDGET); + twin_cache_get_stats(&st1); + draw_widgets(b, 7, 3); + twin_cache_get_stats(&st2); + CHECK(!memcmp(a->p.v, b->p.v, bytes), + "translated cache hit changes pixels"); + CHECK(st2.hits - st1.hits >= 3, "translated rounded rects hit the cache"); + + /* A tiny budget evicts but never corrupts */ + twin_cache_set_budget(600); + draw_widgets(b, 0, 0); + draw_widgets(a, 0, 0); + CHECK(!memcmp(a->p.v, b->p.v, bytes), "eviction churn changes pixels"); + twin_cache_get_stats(&st2); + CHECK(st2.bytes <= 600, "tiny budget honored (%zu)", st2.bytes); + twin_cache_set_budget((size_t) CONFIG_CACHE_SIZE * 1024); + + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); +} + +static void test_text(void) +{ + twin_pixmap_t *p = twin_pixmap_create(TWIN_ARGB32, W, 40); + twin_font_t *font = NULL; +#if defined(CONFIG_FONT_MONTSERRAT) + font = &twin_font_montserrat; +#endif + const char *msg = "Messages 12:03"; + twin_fixed_t size = twin_int_to_fixed(14); + twin_fixed_t w = twin_text_width(font, size, msg); + twin_fill(p, 0xffffffff, TWIN_SOURCE, 0, 0, W, 40); + twin_fixed_t adv = twin_text_draw(p, font, size, twin_int_to_fixed(2), + twin_int_to_fixed(20), 0xff000000, msg); + CHECK_EQ(adv, w, "draw advance equals measured width"); + + /* The path API measures with the font a path selects */ + twin_path_t *path = twin_path_create(); + twin_path_set_font(path, font); + twin_path_set_font_size(path, size); + CHECK_EQ(twin_width_utf8(path, msg), w, "path font matches text width"); + twin_path_destroy(path); + CHECK(w > twin_int_to_fixed(60) && w < twin_int_to_fixed(140), + "plausible width %d", twin_fixed_to_int(w)); + + int ink = 0; + for (int y = 0; y < 40; y++) + for (int x = 0; x < W; x++) + if (*twin_pixmap_pointer(p, x, y).argb32 != 0xffffffff) + ink++; + CHECK(ink > 100, "text left ink (%d pixels)", ink); + + /* Vertically clipped out: no pixels, same advance */ + twin_pixmap_t *q = twin_pixmap_create(TWIN_ARGB32, W, 40); + twin_fill(q, 0xffffffff, TWIN_SOURCE, 0, 0, W, 40); + twin_pixmap_clip(q, 0, 30, W, 40); + adv = twin_text_draw(q, font, size, 0, twin_int_to_fixed(10), 0xff000000, + msg); + CHECK_EQ(adv, w, "clipped-out text still measures"); + + /* Horizontal clip: pixels inside equal the unclipped rendering */ + twin_pixmap_reset_clip(q); + twin_fill(q, 0xffffffff, TWIN_SOURCE, 0, 0, W, 40); + twin_pixmap_clip(q, 20, 0, 50, 40); + twin_text_draw(q, font, size, twin_int_to_fixed(2), twin_int_to_fixed(20), + 0xff000000, msg); + bool same = true; + for (int y = 0; y < 40; y++) + for (int x = 20; x < 50; x++) + if (*twin_pixmap_pointer(p, x, y).argb32 != + *twin_pixmap_pointer(q, x, y).argb32) + same = false; + CHECK(same, "clipped text matches unclipped inside the clip"); + + twin_fixed_t asc, desc; + twin_font_extents(font, size, &asc, &desc); + CHECK(asc > 0 && desc > 0, "font extents positive"); + /* Stroke fonts draw U+0000 from offset 0; TrueType tables reserve it */ + CHECK(twin_has_ucs4(g_twin_font, 0), "stroke font has U+0000"); +#if defined(CONFIG_FONT_MONTSERRAT) + CHECK(!twin_has_ucs4(&twin_font_montserrat, 0), "TTF lacks U+0000"); + + /* TrueType bearings come from the outline: 'j' inks left of the pen */ + twin_path_t *mp = twin_path_create(); + twin_path_set_font(mp, &twin_font_montserrat); + twin_path_set_font_size(mp, size); + twin_text_metrics_t jm, om; + twin_text_metrics_utf8(mp, "j", &jm); + CHECK(jm.left_side_bearing < 0, "TTF 'j' has a negative left bearing"); + twin_text_metrics_utf8(mp, "o", &om); + CHECK(om.left_side_bearing > 0 && om.right_side_bearing < om.width, + "TTF 'o' ink lies inside its advance"); + twin_path_destroy(mp); +#endif +#if defined(CONFIG_FONT_ICONS) + CHECK(!twin_has_ucs4(&twin_font_icons, 0), "icon font lacks U+0000"); + CHECK(twin_has_ucs4(&twin_font_icons, 0xf095), "icon font has phone"); + CHECK(!twin_has_ucs4(&twin_font_icons, 0xf096), "icon font lacks U+F096"); + CHECK(twin_text_width(&twin_font_icons, size, TWIN_SYM_PHONE) > 0, + "icon advance"); +#endif + twin_pixmap_destroy(p); + twin_pixmap_destroy(q); +} + +static void test_gradient(void) +{ + twin_pixmap_t *p = twin_pixmap_create(TWIN_ARGB32, 64, 64); + twin_path_t *path = twin_path_create(); + twin_path_rectangle(path, 0, 0, twin_int_to_fixed(64), + twin_int_to_fixed(64)); + + twin_paint_path_linear(p, path, 0, 0, 0xffff0000, 0, twin_int_to_fixed(64), + 0xff0000ff); + twin_argb32_t top = *twin_pixmap_pointer(p, 32, 0).argb32; + twin_argb32_t mid = *twin_pixmap_pointer(p, 32, 32).argb32; + twin_argb32_t bot = *twin_pixmap_pointer(p, 32, 63).argb32; + CHECK(((top >> 16) & 0xff) > 0xf0 && (top & 0xff) < 0x10, "top red %08x", + top); + CHECK(((bot >> 16) & 0xff) < 0x10 && (bot & 0xff) > 0xf0, + "bottom blue %08x", bot); + CHECK(((mid >> 16) & 0xff) > 0x60 && ((mid >> 16) & 0xff) < 0xa0, + "middle blended %08x", mid); + CHECK(*twin_pixmap_pointer(p, 0, 32).argb32 == mid, + "vertical gradient constant along rows"); + + twin_paint_path_linear(p, path, 0, 0, 0xff000000, twin_int_to_fixed(64), 0, + 0xffffffff); + twin_argb32_t l = *twin_pixmap_pointer(p, 0, 10).argb32; + twin_argb32_t r = *twin_pixmap_pointer(p, 63, 10).argb32; + CHECK((l & 0xff) < 0x10 && (r & 0xff) > 0xf0, "horizontal %08x -> %08x", l, + r); + CHECK(*twin_pixmap_pointer(p, 40, 0).argb32 == + *twin_pixmap_pointer(p, 40, 50).argb32, + "horizontal gradient constant along columns"); + twin_path_destroy(path); + twin_pixmap_destroy(p); +} + +/* The per-pixel formula the stepped gradient must reproduce exactly */ +static twin_argb32_t ref_gradient(twin_coord_t x, + twin_coord_t y, + twin_fixed_t x0, + twin_fixed_t y0, + twin_argb32_t c0, + twin_fixed_t x1, + twin_fixed_t y1, + twin_argb32_t c1) +{ + int64_t gx = (int64_t) x1 - x0, gy = (int64_t) y1 - y0; + int64_t len2 = (gx * gx + gy * gy) >> 16; + twin_fixed_t cx = twin_int_to_fixed(x) + TWIN_FIXED_HALF; + twin_fixed_t cy = twin_int_to_fixed(y) + TWIN_FIXED_HALF; + int64_t dot = (((int64_t) cx - x0) * gx + ((int64_t) cy - y0) * gy) >> 16; + int64_t t = (dot * 65536) / len2; + if (t <= 0) + return c0; + if (t >= TWIN_FIXED_ONE) + return c1; + twin_argb32_t out = 0; + for (int shift = 0; shift < 32; shift += 8) { + int32_t a = (c0 >> shift) & 0xff, b = (c1 >> shift) & 0xff; + int32_t v = a + (int32_t) (((int64_t) (b - a) * t + 0x8000) >> 16); + out |= (twin_argb32_t) (v & 0xff) << shift; + } + return out; +} + +/* Diagonal, reversed, steep and off-canvas gradients match the formula at + * every pixel */ +static void test_gradient_exact(void) +{ + static const struct { + int x0, y0, x1, y1; + } dirs[] = { + {0, 0, 63, 63}, {63, 0, 0, 63}, {50, 60, 3, 2}, {10, 0, 13, 64}, + {-40, 7, 90, 30}, {5, 5, 6, 5}, {32, 32, 31, 0}, {0, 70, 1, -20}, + }; + twin_pixmap_t *p = twin_pixmap_create(TWIN_ARGB32, 64, 64); + twin_path_t *path = twin_path_create(); + twin_path_rectangle(path, 0, 0, twin_int_to_fixed(64), + twin_int_to_fixed(64)); + const twin_argb32_t c0 = 0xff10a0f0, c1 = 0xfff02010; + for (size_t d = 0; d < sizeof(dirs) / sizeof(dirs[0]); d++) { + twin_fixed_t x0 = twin_int_to_fixed(dirs[d].x0) + 0x1234; + twin_fixed_t y0 = twin_int_to_fixed(dirs[d].y0) - 0x0567; + twin_fixed_t x1 = twin_int_to_fixed(dirs[d].x1); + twin_fixed_t y1 = twin_int_to_fixed(dirs[d].y1) + 0x89ab; + twin_paint_path_linear(p, path, x0, y0, c0, x1, y1, c1); + int bad = 0; + for (twin_coord_t y = 0; y < 64; y++) + for (twin_coord_t x = 0; x < 64; x++) + bad += *twin_pixmap_pointer(p, x, y).argb32 != + ref_gradient(x, y, x0, y0, c0, x1, y1, c1); + CHECK(!bad, "gradient %zu: %d pixels off the formula", d, bad); + } + twin_path_destroy(path); + twin_pixmap_destroy(p); +} + +typedef struct { + int calls; + twin_fixed_t last; + bool monotonic; + int stop_after; +} tween_probe_t; + +static bool tween_probe(twin_fixed_t v, void *closure) +{ + tween_probe_t *pr = closure; + if (pr->calls && v < pr->last) + pr->monotonic = false; + pr->last = v; + pr->calls++; + return !pr->stop_after || pr->calls < pr->stop_after; +} + +typedef struct { + twin_tween_t *self; + int calls; + twin_fixed_t last; +} finish_probe_t; + +static bool finish_and_stop(twin_fixed_t v, void *closure) +{ + finish_probe_t *f = closure; + f->last = v; + if (++f->calls == 2) { + twin_tween_finish(f->self); + return false; + } + return true; +} + +static void test_tween(void) +{ + th_t *t = th_create(W, H); + tween_probe_t a = {.monotonic = true}; + twin_tween_start(200, TwinEaseOutCubic, tween_probe, &a); + th_advance(t, 400); + CHECK_EQ(a.last, TWIN_FIXED_ONE, "tween ends at one"); + CHECK(a.monotonic, "ease-out is monotonic"); + CHECK(a.calls >= 10 && a.calls <= 16, "about one call per frame (%d)", + a.calls); + + tween_probe_t b = {.monotonic = true}; + twin_tween_t *tw = twin_tween_start(200, TwinEaseLinear, tween_probe, &b); + th_advance(t, 48); + int seen = b.calls; + twin_tween_cancel(tw); + th_advance(t, 400); + CHECK_EQ(b.calls, seen, "cancelled tween stops"); + + tween_probe_t c = {.monotonic = true}; + tw = twin_tween_start(1000, TwinEaseInOutCubic, tween_probe, &c); + th_advance(t, 32); + twin_tween_finish(tw); + CHECK_EQ(c.last, TWIN_FIXED_ONE, "finish jumps to the end"); + int after = c.calls; + th_advance(t, 200); + CHECK_EQ(c.calls, after, "finished tween is gone"); + + tween_probe_t d = {.monotonic = true, .stop_after = 3}; + twin_tween_start(1000, TwinEaseLinear, tween_probe, &d); + th_advance(t, 400); + CHECK_EQ(d.calls, 3, "returning false stops the tween"); + + /* finish() from the step callback delivers the end value even when the + * callback also returns false */ + finish_probe_t f = {0}; + f.self = twin_tween_start(1000, TwinEaseLinear, finish_and_stop, &f); + th_advance(t, 100); + CHECK_EQ(f.last, TWIN_FIXED_ONE, "finish-and-stop delivers the end value"); + CHECK_EQ(f.calls, 3, "end value delivered once, then stopped"); + th_advance(t, 200); + CHECK_EQ(f.calls, 3, "finish-and-stop tween is gone"); + + CHECK(twin_ease(TwinEaseOutBack, TWIN_FIXED_ONE * 3 / 4) > TWIN_FIXED_ONE, + "ease-out-back overshoots"); + th_destroy(t); +} + +static twin_time_t never(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; + return 1000; +} + +static int fired; +static twin_time_t once(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; + fired++; + return -1; +} + +static void test_closure_churn(void) +{ + th_t *t = th_create(W, H); + /* Far more distinct closures than the tracker holds at once */ + for (int i = 0; i < 4 * TWIN_CLOSURE_INLINE; i++) { + void *owner = malloc(16); + twin_timeout_t *to = twin_set_timeout(never, 1000, owner); + CHECK(to != NULL, "timeout %d scheduled", i); + twin_clear_timeout(to); + free(owner); + if (!to) + break; + } + th_frame(t); + CHECK_EQ(_twin_closure_tracker.count, 0, "tracker drains with its holders"); + + /* Many live closures at once: the table grows rather than refusing */ + enum { LIVE = 8 * TWIN_CLOSURE_INLINE }; + static char owners[LIVE]; + twin_timeout_t *live[LIVE]; + for (int i = 0; i < LIVE; i++) + live[i] = twin_set_timeout(never, 1000, &owners[i]); + bool all = true; + for (int i = 0; i < LIVE; i++) + all = all && live[i] && _twin_closure_is_valid(&owners[i]); + CHECK(all, "every live closure tracked past the inline slots"); + for (int i = 0; i < LIVE; i++) + twin_clear_timeout(live[i]); + CHECK(_twin_closure_tracker.entries == _twin_closure_tracker.inline_entries, + "drained table returns to inline storage"); + + /* A timeout whose owner went away is dropped, not retried forever */ + int owner; + fired = 0; + twin_set_timeout(once, 10, &owner); + _twin_closure_mark_for_free(&owner); + _twin_closure_unregister(&owner); + th_advance(t, 50); + CHECK_EQ(fired, 0, "dead closure never fires"); + CHECK(_twin_timeout_delay() < 0, "dead timeout removed from the queue"); + + /* NULL closures run */ + fired = 0; + twin_set_timeout(once, 5, NULL); + th_advance(t, 20); + CHECK_EQ(fired, 1, "timeout without closure fires"); + th_destroy(t); +} + +typedef struct { + int offset; + int highlight; /* row index painted highlighted, -1 for none */ +} scroll_state_t; + +static void scroll_draw(twin_window_t *w) +{ + scroll_state_t *s = w->client_data; + twin_pixmap_t *p = w->pixmap; + for (int row = -1; row < 12; row++) { + int y = row * 12 - (s->offset % 12); + int idx = row + s->offset / 12; + twin_argb32_t bg = (idx & 1) ? 0xffe0e0f0 : 0xffffffff; + if (idx == s->highlight) + bg = 0xffffc040; + twin_fill(p, bg, TWIN_SOURCE, 0, y, 80, y + 12); + char label[16]; + snprintf(label, sizeof(label), "row %d", idx); + twin_text_draw(p, NULL, twin_int_to_fixed(9), twin_int_to_fixed(3), + twin_int_to_fixed(y + 10), 0xff000000, label); + } +} + +static void test_scroll(void) +{ + th_t *t = th_create(W, H); + scroll_state_t st = {0, -1}; + twin_window_t *w = twin_window_create(t->screen, TWIN_ARGB32, + TwinWindowPlain, 10, 10, 80, 96); + w->client_data = &st; + w->draw = scroll_draw; + twin_window_show(w); + twin_window_draw(w); + th_frame(t); + + for (int step = 0; step < 6; step++) { + int d = 5 + step * 3; + if (step == 5) { + /* Change a row and queue its repaint without painting yet: the + * scroll must not carry the stale pixels away from the damage + * (last step, so the moved pixels stay in view) */ + st.highlight = 1 + st.offset / 12 + 2; + int y = 2 * 12 - (st.offset % 12) + 12; + twin_window_damage(w, w->client.left, w->client.top + y, + w->client.right, w->client.top + y + 12); + twin_window_queue_paint(w); + } + st.offset += d; + twin_window_scroll(w, (twin_rect_t) {0, 80, 0, 96}, 0, -d); + th_frame(t); + } + size_t n = (size_t) w->pixmap->stride * w->pixmap->height; + uint8_t *scrolled = malloc(n); + memcpy(scrolled, w->pixmap->p.v, n); + + twin_window_damage(w, 0, 0, 80, 96); + twin_window_draw(w); + CHECK(!memcmp(scrolled, w->pixmap->p.v, n), + "blit scrolling equals a full repaint"); + free(scrolled); + CHECK_EQ(th_damage_check(t), 0, "scroll damage consistent"); + twin_window_destroy(w); + th_destroy(t); +} + +typedef struct { + int calls; + twin_area_t area; /* sum of clip areas seen by draw() */ +} damage_probe_t; + +static void probe_draw(twin_window_t *w) +{ + damage_probe_t *p = w->client_data; + twin_rect_t c = twin_pixmap_get_clip(w->pixmap); + p->calls++; + p->area += (twin_area_t) (c.right - c.left) * (c.bottom - c.top); + twin_fill(w->pixmap, 0xff808080, TWIN_SOURCE, c.left, c.top, c.right, + c.bottom); +} + +static void test_window_damage_rects(void) +{ + th_t *t = th_create(W, H); + damage_probe_t probe = {0}; + twin_window_t *w = + twin_window_create(t->screen, TWIN_ARGB32, TwinWindowPlain, 0, 0, W, H); + w->client_data = &probe; + w->draw = probe_draw; + twin_window_show(w); + twin_window_draw(w); + th_frame(t); + + probe = (damage_probe_t) {0}; + twin_window_damage(w, 2, 2, 12, 12); + twin_window_damage(w, W - 12, H - 12, W - 2, H - 2); + twin_window_draw(w); + CHECK_EQ(probe.calls, 2, "distant damage paints two rectangles"); + CHECK_EQ(probe.area, 200, "only the damaged pixels are repainted"); + + probe = (damage_probe_t) {0}; + twin_window_damage(w, 10, 10, 20, 20); + twin_window_damage(w, 20, 10, 30, 20); /* adjacent: merges for free */ + twin_window_draw(w); + CHECK_EQ(probe.calls, 1, "adjacent damage merges"); + + probe = (damage_probe_t) {0}; + for (int i = 0; i < 10; i++) + twin_window_damage(w, i * 15, i * 11, i * 15 + 4, i * 11 + 4); + twin_window_draw(w); + CHECK(probe.calls <= TWIN_WINDOW_DAMAGE_RECTS, "damage list bounded (%d)", + probe.calls); + th_frame(t); + CHECK_EQ(th_damage_check(t), 0, "multi-rect window damage consistent"); + twin_window_destroy(w); + th_destroy(t); +} + +/* Scaled composites must sample the same source pixel whether or not the + * destination clip cuts the operation short */ +static void test_scaled_clip(void) +{ + twin_pixmap_t *src = twin_pixmap_create(TWIN_ARGB32, 16, 16); + for (int y = 0; y < 16; y++) + for (int x = 0; x < 16; x++) + *twin_pixmap_pointer(src, x, y).argb32 = + 0xff000000 | (x * 16) << 16 | (y * 16) << 8; + /* Destination pixel = source pixel / 2: a 2x magnification */ + twin_matrix_scale(&src->transform, TWIN_FIXED_HALF, TWIN_FIXED_HALF); + twin_operand_t op = {.source_kind = TWIN_PIXMAP, .u.pixmap = src}; + + twin_pixmap_t *a = twin_pixmap_create(TWIN_ARGB32, 64, 64); + twin_pixmap_t *b = twin_pixmap_create(TWIN_ARGB32, 64, 64); + twin_composite(a, 10, 10, &op, 0, 0, NULL, 0, 0, TWIN_SOURCE, 32, 32); + twin_pixmap_clip(b, 25, 20, 40, 38); + twin_composite(b, 10, 10, &op, 0, 0, NULL, 0, 0, TWIN_SOURCE, 32, 32); + + bool same = true; + for (int y = 20; y < 38; y++) + for (int x = 25; x < 40; x++) + if (*twin_pixmap_pointer(a, x, y).argb32 != + *twin_pixmap_pointer(b, x, y).argb32) + same = false; + CHECK(same, "clipped scaled composite samples the right source pixels"); + twin_pixmap_destroy(src); + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); +} + +/* Every pixel of two same-size ARGB32 pixmaps matches */ +static bool same_argb32(twin_pixmap_t *a, twin_pixmap_t *b) +{ + for (twin_coord_t y = 0; y < twin_pixmap_height(a); y++) + if (memcmp(twin_pixmap_pointer(a, 0, y).v, + twin_pixmap_pointer(b, 0, y).v, + (size_t) twin_pixmap_width(a) * 4)) + return false; + return true; +} + +static void test_rgb16_mask(void) +{ + /* RGB16 has no alpha: masking with it equals masking with nothing */ + twin_pixmap_t *src = solid(TWIN_ARGB32, 8, 8, 0x80ff4020); + twin_pixmap_t *msk = solid(TWIN_RGB16, 8, 8, 0xff000000); + twin_pixmap_t *a = solid(TWIN_ARGB32, 8, 8, 0xff102030); + twin_pixmap_t *b = solid(TWIN_ARGB32, 8, 8, 0xff102030); + twin_operand_t sop = {.source_kind = TWIN_PIXMAP, .u.pixmap = src}; + twin_operand_t mop = {.source_kind = TWIN_PIXMAP, .u.pixmap = msk}; + twin_composite(a, 0, 0, &sop, 0, 0, &mop, 0, 0, TWIN_OVER, 8, 8); + twin_composite(b, 0, 0, &sop, 0, 0, NULL, 0, 0, TWIN_OVER, 8, 8); + CHECK(same_argb32(a, b), "RGB16 mask is opaque"); + twin_pixmap_destroy(src); + twin_pixmap_destroy(msk); + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); +} + +/* A transformed RGB16 source reads the same pixels as its ARGB32 copy, and + * transparency outside it, all the way down the pixmap */ +static void test_rgb16_xform(void) +{ + twin_pixmap_t *s16 = twin_pixmap_create(TWIN_RGB16, 16, 16); + twin_pixmap_t *s32 = twin_pixmap_create(TWIN_ARGB32, 16, 16); + for (int y = 0; y < 16; y++) { + twin_argb32_t c = 0xff000000 | (twin_argb32_t) (y * 16) << 16 | + (twin_argb32_t) (255 - y * 16); + twin_fill(s16, c, TWIN_SOURCE, 0, y, 16, y + 1); + } + /* The ARGB32 copy holds the exact RGB565 values, so only the read path + * differs */ + for (int y = 0; y < 16; y++) + for (int x = 0; x < 16; x++) + *twin_pixmap_pointer(s32, x, y).argb32 = + twin_rgb16_to_argb32(*twin_pixmap_pointer(s16, x, y).rgb16); + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_translate(&m, twin_int_to_fixed(4), twin_int_to_fixed(4)); + twin_pixmap_set_transform(s16, &m); + twin_pixmap_set_transform(s32, &m); + twin_operand_t o16 = {.source_kind = TWIN_PIXMAP, .u.pixmap = s16}; + twin_operand_t o32 = {.source_kind = TWIN_PIXMAP, .u.pixmap = s32}; + twin_pixmap_t *a = solid(TWIN_ARGB32, 16, 16, 0xff808080); + twin_pixmap_t *b = solid(TWIN_ARGB32, 16, 16, 0xff808080); + twin_composite(a, 0, 0, &o16, 0, 0, NULL, 0, 0, TWIN_OVER, 16, 16); + twin_composite(b, 0, 0, &o32, 0, 0, NULL, 0, 0, TWIN_OVER, 16, 16); + CHECK(same_argb32(a, b), + "transformed RGB16 source matches its ARGB32 copy"); + twin_pixmap_destroy(s16); + twin_pixmap_destroy(s32); + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); +} + +/* A translated source transform shifts which source pixels land where, + * with every renderer (pixman used to drop the translation) */ +static void test_translated_source(void) +{ + twin_pixmap_t *src = twin_pixmap_create(TWIN_ARGB32, 16, 16); + twin_fill(src, 0x00000000, TWIN_SOURCE, 0, 0, 16, 16); + twin_fill(src, 0xffff0000, TWIN_SOURCE, 8, 8, 16, 16); + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_translate(&m, twin_int_to_fixed(8), twin_int_to_fixed(8)); + twin_pixmap_set_transform(src, &m); + twin_operand_t op = {.source_kind = TWIN_PIXMAP, .u.pixmap = src}; + + twin_pixmap_t *dst = twin_pixmap_create(TWIN_ARGB32, 16, 16); + twin_fill(dst, 0xff000000, TWIN_SOURCE, 0, 0, 16, 16); + twin_composite(dst, 0, 0, &op, 0, 0, NULL, 0, 0, TWIN_SOURCE, 6, 6); + CHECK_EQ(*twin_pixmap_pointer(dst, 2, 2).argb32, 0xffff0000, + "translated source: (2,2) reads source (10,10)"); + twin_pixmap_destroy(src); + twin_pixmap_destroy(dst); +} + +static void test_leaks(void) +{ + th_t *t = th_create(W, H); + th_frame(t); + size_t base = th_heap_net(t); + for (int i = 0; i < 20; i++) { + twin_window_t *w = twin_window_create( + t->screen, TWIN_ARGB32, TwinWindowApplication, 0, 0, 50, 50); + twin_window_show(w); + twin_window_queue_paint(w); + twin_tween_start(100, TwinEaseLinear, tween_probe, + &(tween_probe_t) {0}); + th_advance(t, 20); + twin_window_destroy(w); /* with a paint still queued */ + th_advance(t, 200); + } + th_frame(t); + CHECK(th_heap_net(t) <= base, "heap back to baseline (%zu > %zu)", + th_heap_net(t), base); + +#if defined(CONFIG_MEMORY_STATS) + /* Injected failures reach realloc too, leaving the block intact */ + char *buf = twin_malloc(16); + CHECK(buf, "malloc"); + if (buf) { + buf[0] = 'x'; + _twin_alloc_fail_next = 1; + CHECK(!twin_realloc(buf, 64), "injected realloc failure"); + CHECK_EQ(_twin_alloc_fail_next, 0, "realloc consumed the failure"); + _twin_alloc_fail_next = 0; + CHECK_EQ(buf[0], 'x', "block kept after a failed realloc"); + twin_free(buf); + } + CHECK(th_heap_net(t) <= base, "failed realloc leaks (%zu > %zu)", + th_heap_net(t), base); +#endif + th_destroy(t); +} + +#if defined(CONFIG_WIDGETS) +static int destroyed_callbacks; + +static twin_dispatch_result_t count_destroy(twin_widget_t *widget, + twin_event_t *event, + void *data) +{ + (void) widget; + (void) data; + if (event->kind == TwinEventDestroy) + destroyed_callbacks++; + return TwinDispatchContinue; +} + +static twin_dispatch_result_t custom_handler(twin_custom_widget_t *custom, + twin_event_t *event) +{ + (void) custom; + (void) event; + return TwinDispatchContinue; +} + +/* Destroying a toplevel releases every widget the toolkit created for it */ +static void test_widget_teardown(void) +{ + th_t *t = th_create(W, H); + th_frame(t); + size_t base = th_heap_net(t); + for (int round = 0; round < 3; round++) { + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_box_t *row = twin_box_create(&top->box, TwinBoxHorz); + twin_label_t *label = twin_label_create( + row, "label", 0xff000000, twin_int_to_fixed(12), TwinStyleRoman); + twin_button_t *button = twin_button_create( + row, "button", 0xff000000, twin_int_to_fixed(12), TwinStyleRoman); + twin_widget_set_callback(&button->label.widget, count_destroy, NULL); + twin_custom_widget_create(&top->box, 0xffffffff, 10, 10, custom_handler, + 64); + (void) label; + twin_toplevel_show(top); + th_frame(t); + destroyed_callbacks = 0; + twin_window_destroy(top->box.widget.window); + th_frame(t); + CHECK_EQ(destroyed_callbacks, 1, "callbacks see the widget's destroy"); + } + CHECK(th_heap_net(t) <= base, "toolkit frees its widgets (%zu > %zu)", + th_heap_net(t), base); + th_destroy(t); +} +#endif + +/* A timeout cleared by another one in the same pass must not run */ +static twin_timeout_t *victim; +static int victim_runs; +static twin_time_t clear_victim(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; + twin_clear_timeout(victim); + return -1; +} +static twin_time_t victim_proc(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; + victim_runs++; + return -1; +} + +static twin_tween_t *victim_tween; +static twin_time_t cancel_victim_tween(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; + twin_tween_cancel(victim_tween); + victim_tween = NULL; + return -1; +} + +static void test_cleared_in_pass(void) +{ + th_t *t = th_create(W, H); + int a, b; + victim_runs = 0; + twin_set_timeout(clear_victim, 0, &a); + victim = twin_set_timeout(victim_proc, 5, &b); + th_advance(t, 20); + CHECK_EQ(victim_runs, 0, "timeout cleared earlier in the pass stays dead"); + + /* Same for a tween cancelled by a timeout due in the same pass, just + * ahead of the tween's own tick */ + tween_probe_t p = {.monotonic = true}; + victim_tween = twin_tween_start(100, TwinEaseLinear, tween_probe, &p); + th_advance(t, 16); /* first tick ran; next one due 16 ms from now */ + int calls = p.calls; + CHECK_EQ(calls, 1, "tween ticked once"); + twin_set_timeout(cancel_victim_tween, 15, &a); + th_advance(t, 16); /* both due: the canceller runs first */ + CHECK(!victim_tween, "canceller ran"); + CHECK_EQ(p.calls, calls, "tween cancelled in the pass does not tick"); + th_advance(t, 50); + CHECK_EQ(p.calls, calls, "cancelled tween never ticks again"); + th_destroy(t); +} + +/* Clipped text must equal unclipped text inside the clip, including ink + * that overshoots the font extents or starts left of the pen */ +static void test_text_clip_overshoot(void) +{ + twin_font_t *fonts[] = {NULL, +#if defined(CONFIG_FONT_MONTSERRAT) + &twin_font_montserrat +#endif + }; + for (size_t f = 0; f < sizeof(fonts) / sizeof(fonts[0]); f++) { + /* Rows above the nominal ascent; ink left of a pen past the clip */ + static const struct { + twin_rect_t clip; + twin_fixed_t x; + const char *text; + int size; + } cases[] = {{{0, 80, 0, 22}, 10, "|#j([", 24}, + {{0, 30, 0, 60}, 31, "j", 32}}; + for (size_t c = 0; c < 2; c++) { + twin_pixmap_t *a = twin_pixmap_create(TWIN_ARGB32, 80, 60); + twin_pixmap_t *b = twin_pixmap_create(TWIN_ARGB32, 80, 60); + twin_rect_t r = cases[c].clip; + twin_pixmap_clip(b, r.left, r.top, r.right, r.bottom); + for (twin_pixmap_t *p = a; p; p = p == a ? b : NULL) + twin_text_draw(p, fonts[f], twin_int_to_fixed(cases[c].size), + twin_int_to_fixed(cases[c].x), + twin_int_to_fixed(40), 0xff000000, + cases[c].text); + bool same = true; + for (int y = r.top; y < r.bottom; y++) + for (int x = r.left; x < r.right; x++) + if (*twin_pixmap_pointer(a, x, y).argb32 != + *twin_pixmap_pointer(b, x, y).argb32) + same = false; + CHECK(same, "font %zu case %zu: clipped text matches", f, c); + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); + } + } +} + +/* A destroyed window's queued repaint must not run, even if its address + * is registered again by an unrelated owner (address reuse) */ +static int stale_draws; +static void stale_draw(twin_window_t *w) +{ + (void) w; + stale_draws++; +} + +static void test_destroyed_paint(void) +{ + th_t *t = th_create(W, H); + twin_window_t *w = twin_window_create(t->screen, TWIN_ARGB32, + TwinWindowPlain, 0, 0, 50, 50); + w->draw = stale_draw; + stale_draws = 0; + twin_window_queue_paint(w); + void *addr = w; + twin_window_destroy(w); + _twin_closure_register(addr); /* as if a new object took the address */ + th_frame(t); + CHECK_EQ(stale_draws, 0, "queued repaint of a destroyed window never runs"); + _twin_closure_unregister(addr); + th_destroy(t); +} + +/* The public accessors report the fields they stand for */ +static bool on_draw_called; +static void on_draw(twin_window_t *window) +{ + (void) window; + on_draw_called = true; +} + +static void test_accessors(void) +{ + 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"); + + 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 && + twin_pixmap_format(p) == TWIN_RGB16, + "pixmap size and format"); + CHECK(twin_pixmap_pixels(p, &stride) == p->p.v && stride == p->stride, + "pixmap pixels and stride"); + CHECK(twin_pixmap_pixels(p, NULL) == p->p.v, "stride is optional"); + CHECK(!twin_pixmap_animation(p), "still pixmap has no animation"); + twin_matrix_t m; + twin_matrix_identity(&m); + twin_matrix_scale(&m, TWIN_FIXED_HALF, TWIN_FIXED_ONE * 2); + twin_pixmap_set_transform(p, &m); + CHECK(!memcmp(&p->transform, &m, sizeof(m)), "transform set"); + twin_pixmap_set_transform(p, NULL); + CHECK(twin_matrix_is_identity(&p->transform), "NULL resets the transform"); + twin_pixmap_destroy(p); + + twin_window_t *w = twin_window_create(t->screen, TWIN_ARGB32, + TwinWindowPlain, 3, 4, 20, 10); + CHECK(twin_window_pixmap(w) == w->pixmap && + twin_window_style(w) == TwinWindowPlain, + "window pixmap and style"); + twin_rect_t c = twin_window_client(w); + CHECK(!memcmp(&c, &w->client, sizeof(c)), "window client area"); + CHECK(!twin_window_is_mapped(w), "new window is unmapped"); + int data; + twin_window_set_client_data(w, &data); + CHECK(twin_window_client_data(w) == &data, "client data round-trips"); + twin_window_set_callbacks(w, on_draw, NULL, NULL); + on_draw_called = false; + twin_window_draw(w); + twin_window_show(w); + th_frame(t); + CHECK(twin_window_is_mapped(w) && on_draw_called, + "shown window is mapped and drawn through the callback"); + twin_window_destroy(w); + +#if defined(CONFIG_WIDGETS) + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + CHECK(twin_toplevel_box(top) == &top->box, "toplevel box"); + twin_label_t *label = + twin_label_create(twin_toplevel_box(top), "l", 0xff000000, + twin_int_to_fixed(12), TwinStyleRoman); + twin_button_t *button = + twin_button_create(twin_toplevel_box(top), "b", 0xff000000, + twin_int_to_fixed(12), TwinStyleRoman); + CHECK(twin_label_widget(label) == &label->widget && + twin_button_widget(button) == &button->label.widget, + "label and button widgets"); + twin_label_set_align(label, TwinAlignRight); + twin_widget_set_shape(twin_label_widget(label), TwinShapeEllipse); + twin_widget_match_size(twin_button_widget(button), + twin_label_widget(label)); + CHECK(label->align == TwinAlignRight && + label->widget.shape == TwinShapeEllipse && + button->label.widget.copy_geom == &label->widget, + "widget setters"); + twin_toplevel_show(top); + th_frame(t); + CHECK( + button->label.widget.preferred.width == label->widget.preferred.width && + button->label.widget.preferred.height == + label->widget.preferred.height, + "matched widget takes the source's size"); + twin_window_destroy(top->box.widget.window); +#endif + th_destroy(t); +} + +#if defined(CONFIG_WIDGETS) +static twin_argb32_t client_pixel(twin_window_t *w, + twin_coord_t x, + twin_coord_t y) +{ + twin_rect_t c = twin_window_client(w); + return *twin_pixmap_pointer(w->pixmap, c.left + x, c.top + y).argb32; +} + +/* A toplevel paints every client pixel, so the compositor can treat it as + * opaque; children repaint over their ancestors' background instead of + * punching through it */ +static void test_toplevel_background(void) +{ + th_t *t = th_create(W, H); + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_window_t *w = top->box.widget.window; + twin_label_t *label = + twin_label_create(twin_toplevel_box(top), " ", 0xff000000, + twin_int_to_fixed(12), TwinStyleRoman); + twin_label_t *oval = + twin_label_create(twin_toplevel_box(top), " ", 0xff000000, + twin_int_to_fixed(12), TwinStyleRoman); + twin_widget_t *ow = twin_label_widget(oval); + twin_widget_set(ow, 0x80000000); + twin_widget_set_shape(ow, TwinShapeEllipse); + twin_toplevel_show(top); + th_frame(t); + CHECK(w->opaque, "a toplevel is opaque"); + + twin_rect_t e = label->widget.extents; + CHECK_EQ(client_pixel(w, e.left + 1, e.top + 1), 0xffffffff, + "the root's background under a transparent child"); + twin_label_set(label, " "); + th_frame(t); + CHECK_EQ(client_pixel(w, e.left + 1, e.top + 1), 0xffffffff, + "a repainted child keeps it"); + + /* A translucent shape repaints to the same pixels, not darker */ + twin_rect_t o = ow->extents; + twin_coord_t cx = (o.left + o.right) / 2, cy = (o.top + o.bottom) / 2; + twin_argb32_t center = client_pixel(w, cx, cy); + CHECK(center != 0xffffffff, "the oval is painted"); + for (int i = 0; i < 3; i++) { + twin_label_set(oval, i & 1 ? " " : " "); + th_frame(t); + } + CHECK_EQ(client_pixel(w, cx, cy), center, "oval repainted"); + CHECK_EQ(client_pixel(w, o.left, o.top), 0xffffffff, + "the root's background outside the oval"); + + twin_widget_set(twin_box_widget(twin_toplevel_box(top)), 0x80000000); + CHECK(!w->opaque, "a translucent background makes it translucent"); + twin_widget_set(twin_box_widget(twin_toplevel_box(top)), 0xff102030); + CHECK(w->opaque, "and an opaque one opaque again"); + th_frame(t); + CHECK_EQ(client_pixel(w, e.left + 1, e.top + 1), 0xff102030, + "children repaint over the new background"); + CHECK_EQ(th_damage_check(t), 0, "toplevel background"); + twin_toplevel_destroy(top); + th_destroy(t); +} + +/* 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 */ +static void test_box_background(void) +{ + th_t *t = th_create(W, H); + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_window_t *w = top->box.widget.window; + twin_widget_t *root = twin_box_widget(twin_toplevel_box(top)); + twin_toplevel_show(top); + th_frame(t); + CHECK_EQ(client_pixel(w, 5, 5), 0xffffffff, "an empty toplevel is white"); + twin_widget_set(root, 0xff336699); + th_frame(t); + CHECK_EQ(client_pixel(w, 5, 5), 0xff336699, + "a new background reaches pixels no child covers"); + twin_widget_set_shape(root, TwinShapeEllipse); + th_frame(t); + CHECK(!w->opaque, "a shaped root is translucent"); + CHECK_EQ(client_pixel(w, 0, 0), 0, "outside the root's shape is clear"); + CHECK_EQ(th_damage_check(t), 0, "shaped root"); + twin_toplevel_destroy(top); + + /* A child fills over its box's background, and a shape change on the + * box repaints the child */ + top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + w = top->box.widget.window; + /* root > outer > (box > label, sibling) */ + twin_box_t *outer = twin_box_create(twin_toplevel_box(top), TwinBoxVert); + twin_box_t *box = twin_box_create(outer, TwinBoxVert); + twin_label_t *label = twin_label_create( + box, " ", 0xff000000, twin_int_to_fixed(12), TwinStyleRoman); + twin_label_t *sibling = twin_label_create( + outer, " ", 0xff000000, twin_int_to_fixed(12), TwinStyleRoman); + twin_widget_set(twin_box_widget(box), 0xff00ff00); + twin_widget_set(twin_label_widget(sibling), 0xffff0000); + twin_toplevel_show(top); + th_frame(t); + /* Repainting deep in the tree leaves the siblings alone */ + twin_rect_t o = twin_box_widget(outer)->extents; + twin_rect_t s = twin_label_widget(sibling)->extents; + s.left += o.left, s.top += o.top; + twin_label_set(label, " "); + th_frame(t); + CHECK_EQ(client_pixel(w, s.left + 1, s.top + 1), 0xffff0000, + "a nested repaint keeps the siblings"); + twin_rect_t b = twin_box_widget(box)->extents; + b.left += o.left, b.top += o.top; + twin_rect_t l = twin_label_widget(label)->extents; + CHECK(l.right > l.left + 2 && l.bottom > l.top + 2, "label laid out"); + CHECK_EQ(client_pixel(w, b.left + l.left + 1, b.top + l.top + 1), + 0xff00ff00, "a child fills over its box's background"); + twin_widget_set_shape(twin_box_widget(box), TwinShapeEllipse); + th_frame(t); + CHECK_EQ(client_pixel(w, b.left + l.left + 1, b.top + l.top + 1), + 0xffffffff, "a shape change repaints the children"); + CHECK_EQ(th_damage_check(t), 0, "box background"); + twin_toplevel_destroy(top); + th_destroy(t); +} +#endif + +#if defined(CONFIG_LOADER_GIF) || defined(CONFIG_LOADER_TVG) +/* 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 */ +static bool write_fixture(char *path, const uint8_t *bytes, size_t len) +{ + int fd = mkstemp(path); + CHECK(fd >= 0, "temporary file created"); + if (fd < 0) + return false; + bool ok = write(fd, bytes, len) == (ssize_t) len; + close(fd); + CHECK(ok, "temporary file written"); + if (!ok) + unlink(path); + return ok; +} +#endif + +#if defined(CONFIG_LOADER_TVG) +/* A line of width zero is drawn with a hairline pen, not dropped: 0.01 + * units, one pixel here at 100x */ +static void test_tvg_hairline(void) +{ + const uint8_t doc[] = { + 0x72, 0x56, 1, /* magic, version 1 */ + 0x44, /* scale 4 (1/16 unit), RGBA8888, u8 coords */ + 2, 2, /* 2 x 2 units */ + 1, 0, 0, 0, 255, /* one color: opaque black */ + 0x04, 0, 0, 0, /* draw 1 line, color 0, width 0 */ + 2, 16, 30, 16, /* (0.125, 1) to (1.875, 1) */ + 0x00, /* end */ + }; + char path[] = "/tmp/mado-tvg-XXXXXX"; + if (!write_fixture(path, doc, sizeof(doc))) + return; + twin_pixmap_t *p = twin_tvg_to_pixmap(path, 200, 200); + unlink(path); + CHECK(p != NULL, "TVG with a zero-width line loads"); + if (!p) + return; + int inked = 0; + for (int y = 0; y < twin_pixmap_height(p); y++) + for (int x = 0; x < twin_pixmap_width(p); x++) + inked += (*twin_pixmap_pointer(p, x, y).argb32 >> 24) != 0; + CHECK(inked > 100, "zero-width line drawn as a hairline (%d pixels)", + inked); + twin_pixmap_destroy(p); +} +#endif + +#if defined(CONFIG_LOADER_GIF) +/* Load @len bytes as a file; NULL is the only acceptable answer here */ +static twin_pixmap_t *load_bytes(const uint8_t *bytes, size_t len) +{ + char path[] = "/tmp/mado-gif-XXXXXX"; + if (!write_fixture(path, bytes, len)) + return NULL; + twin_pixmap_t *p = twin_pixmap_from_file(path); + unlink(path); + return 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 */ +static void test_gif_malformed(void) +{ + /* 4x4, black/white palette, a 2x2 image placed at x=100, then EOF */ + uint8_t bad[] = {'G', 'I', 'F', '8', '9', 'a', 4, 0, 4, 0, + 0x80, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, ',', + 100, 0, 0, 0, 2, 0, 2, 0, 0}; + CHECK(!load_bytes(bad, sizeof(bad)), "undecodable frame rejected"); + + uint8_t zero[sizeof(bad)]; + memcpy(zero, bad, sizeof(bad)); + zero[6] = zero[7] = 0; + CHECK(!load_bytes(zero, sizeof(zero)), "zero width rejected"); + + uint8_t huge[sizeof(bad)]; + memcpy(huge, bad, sizeof(bad)); + huge[6] = huge[7] = huge[8] = huge[9] = 0xff; + CHECK(!load_bytes(huge, sizeof(huge)), "65535x65535 rejected"); + + /* Every truncation of a small image returns: a cut inside the data + * sub-blocks used to leave the block size unread and loop forever */ + const uint8_t good[] = {'G', 'I', 'F', '8', '9', 'a', 2, 0, 2, + 0, 0x80, 0, 0, 0, 0, 0, 0xff, 0xff, + 0xff, ',', 0, 0, 0, 0, 2, 0, 2, + 0, 0, 2, 2, 0x44, 0x01, 0, ';'}; + for (size_t cut = 0; cut <= sizeof(good); cut++) { + twin_pixmap_t *p = load_bytes(good, cut); + if (p) + twin_pixmap_destroy(p); + } + /* Reaching here is the check: every truncated GIF returned */ +} +#endif + +int main(void) +{ + test_damage_merge(); + test_occlusion_differential(); + test_row_fallback(); +#if defined(CONFIG_DROP_SHADOW) + test_shadow(); +#endif + test_rgb16_and_opacity(); + test_stub(); + test_cache_equivalence(); + test_text(); + test_gradient(); + test_gradient_exact(); + test_tween(); + test_closure_churn(); + test_scroll(); + test_window_damage_rects(); + test_scaled_clip(); + test_rgb16_mask(); + test_rgb16_xform(); + test_translated_source(); + test_cleared_in_pass(); + test_text_clip_overshoot(); + test_destroyed_paint(); + test_accessors(); +#if defined(CONFIG_WIDGETS) + test_widget_teardown(); + test_toplevel_background(); + test_box_background(); +#endif +#if defined(CONFIG_LOADER_GIF) + test_gif_malformed(); +#endif +#if defined(CONFIG_LOADER_TVG) + test_tvg_hairline(); +#endif + test_leaks(); + twin_cache_flush(); + return th_report("core"); +} diff --git a/tests/test-demos.c b/tests/test-demos.c new file mode 100644 index 00000000..336416a1 --- /dev/null +++ b/tests/test-demos.c @@ -0,0 +1,160 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Every app in the registry (apps.h) under the window manager this build + * has, which is one the app runs under: the registry lists no other. Each + * app is started on a fresh screen, run for a while, poked, and then + * closed window by window the way a user would (Back under the mobile + * policy, close otherwise). Its windows must stay where the policy can + * reach them, frames must match a full recomposite, and closing must return + * every byte. + */ + +#include "apps.h" +#include "harness.h" + +#if defined(CONFIG_WM_MOBILE) +#define SCENE "mobile" +#define W 240 +#define H 320 +#elif defined(CONFIG_WM_DESKTOP) +#define SCENE "desktop" +#define W 640 +#define H 480 +#else +#define SCENE "none" +#define W 640 +#define H 480 +#endif + +/* Where the policy must keep each window: inside the screen, and covering + * the work area exactly under the policies that do not place windows */ +static void check_placement(th_t *t, const char *what) +{ + twin_rect_t area = twin_screen_work_area(t->screen); +#if defined(CONFIG_WM_DESKTOP) + const bool fills = false; +#else + const bool fills = true; +#endif + for (twin_window_t *w = t->screen->windows; w; w = w->wm_next) { + /* Windows a policy keeps hidden (covered on a phone) do not count */ + if (w->style != TwinWindowApplication || !_twin_window_shown(w)) + continue; + twin_pixmap_t *p = w->pixmap; + if (fills) { + CHECK(p->x == area.left && p->y == area.top && + p->width == area.right - area.left && + p->height == area.bottom - area.top, + "%s: window covers the work area (%d,%d %dx%d)", what, p->x, + p->y, p->width, p->height); + } else { + CHECK(p->x >= 0 && p->y >= 0 && p->x + p->width <= t->width && + p->y + p->height <= t->height, + "%s: window on screen (%d,%d %dx%d)", what, p->x, p->y, + p->width, p->height); + } + } +} + +static void run(const apps_entry_t *d) +{ + char what[96]; + snprintf(what, sizeof(what), "%s under %s", d->name, SCENE); + + th_t *t = th_create(W, H); + th_settle(t, 500); + /* First pass warms the glyph and coverage caches; the second measures */ + size_t base = 0; + for (int pass = 0; pass < 2; pass++) { + if (pass) + base = th_heap_net(t); + CHECK(d->start(t->screen, d->name, d->x, d->y, d->w, d->h), + "%s: started", what); + th_settle(t, 2000); + int n; + CHECK(th_top_app(t->screen, &n) && n > 0, "%s: opened a window", what); + check_placement(t, what); + CHECK_EQ(th_damage_check(t), 0, what); + + /* Let timers run (clock hands, animation frames), then poke it */ + th_advance(t, 1500); + th_settle(t, 1000); + th_tap(t, W / 2, H / 2); + th_settle(t, 1000); + CHECK_EQ(th_damage_check(t), 0, what); + + /* Close what the demo opened, one window at a time */ + for (int guard = 0; guard < 16; guard++) { + twin_window_t *top = th_top_app(t->screen, NULL); + if (!top) + break; +#if defined(CONFIG_WM_MOBILE) + twin_window_back(top); +#else + twin_window_close(top); +#endif + th_settle(t, 2000); + twin_window_t *next = th_top_app(t->screen, NULL); + if (next && next->pixmap && _twin_window_shown(next)) + CHECK(th_painted(t, next->pixmap), + "%s: revealed window painted", what); + } + CHECK(!th_top_app(t->screen, &n), "%s: every window closed (%d left)", + what, n); + CHECK_EQ(th_damage_check(t), 0, what); + } +#if defined(CONFIG_MEMORY_STATS) + CHECK(th_heap_net(t) <= base, "%s: closing leaks (%zu > %zu)", what, + th_heap_net(t), base); +#else + (void) base; +#endif + th_destroy(t); +} + +#if defined(CONFIG_MEMORY_STATS) +/* Out of memory from the first allocation: the start reports failure and + * leaves neither a window nor a byte behind. (Failures deeper in, while + * text and paths are painted, are not handled by the core yet.) */ +static void run_out_of_memory(const apps_entry_t *d) +{ + th_t *t = th_create(W, H); + th_settle(t, 500); + size_t base = th_heap_net(t); + _twin_alloc_fail_next = 1; + bool ok = d->start(t->screen, d->name, d->x, d->y, d->w, d->h); + _twin_alloc_fail_next = 0; + int n; + th_top_app(t->screen, &n); + /* An app of several windows may still open the others */ + CHECK(ok || !n, "%s: failed start left %d windows", d->name, n); + th_settle(t, 500); + for (twin_window_t *w; (w = th_top_app(t->screen, NULL));) + twin_window_destroy(w); + th_settle(t, 500); + CHECK(th_heap_net(t) <= base, "%s: failed start leaks (%zu > %zu)", d->name, + th_heap_net(t), base); + th_destroy(t); +} +#endif + +int main(void) +{ + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *d = apps_at(i); + /* Shells own the screen and have suites of their own */ + if (d->boot) + continue; + run(d); +#if defined(CONFIG_MEMORY_STATS) + run_out_of_memory(d); +#endif + } + twin_cache_flush(); + return th_report("demos"); +} diff --git a/tests/test-lifetime.c b/tests/test-lifetime.c new file mode 100644 index 00000000..dc1567dc --- /dev/null +++ b/tests/test-lifetime.c @@ -0,0 +1,407 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Lifetime and re-entrancy regressions: callbacks that run while an object + * is being torn down, or that act on the object currently calling them. + * Each test checks state rather than relying on a crash, so a regression + * fails the check in plain builds and shows up under ASan as well. + */ + +#include +#include + +#include "harness.h" + +#define W 160 +#define H 120 + +typedef struct { + int draws, activates, deactivates; +} probe_t; + +static void probe_draw(twin_window_t *w) +{ + probe_t *p = w->client_data; + p->draws++; + twin_fill(w->pixmap, 0xff336699, TWIN_SOURCE, 0, 0, + w->client.right - w->client.left, + w->client.bottom - w->client.top); +} + +static bool probe_event(twin_window_t *w, twin_event_t *ev) +{ + probe_t *p = w->client_data; + if (ev->kind == TwinEventActivate) + p->activates++; + else if (ev->kind == TwinEventDeactivate) + p->deactivates++; + return false; +} + +static twin_window_t *probe_window(th_t *t, + probe_t *p, + twin_window_style_t style, + twin_coord_t x, + twin_coord_t y) +{ + twin_window_t *w = + twin_window_create(t->screen, TWIN_ARGB32, style, x, y, 60, 40); + w->client_data = p; + w->draw = probe_draw; + w->event = probe_event; + return w; +} + +/* + * A window's destroy callback schedules a timeout and a work item on the + * window itself. Neither may fire once the window is freed. + */ +static int late_timeouts, late_works; + +static twin_time_t late_timeout(twin_time_t now, void *closure) +{ + (void) now; + (void) closure; /* never dereferenced: it may be freed */ + late_timeouts++; + return -1; +} + +static bool late_work(void *closure) +{ + (void) closure; + late_works++; + return false; +} + +static void schedule_on_destroy(twin_window_t *w) +{ + twin_set_timeout(late_timeout, 0, w); + twin_set_work(late_work, TWIN_WORK_PAINT, w); +} + +static void test_window_destroy_schedules(void) +{ + th_t *t = th_create(W, H); + probe_t p = {0}; + twin_window_t *w = probe_window(t, &p, TwinWindowApplication, 0, 0); + w->destroy = schedule_on_destroy; + twin_window_show(w); + th_frame(t); + + void *addr = w; + late_timeouts = late_works = 0; + twin_window_destroy(w); + CHECK(!_twin_closure_is_valid(addr), + "destroyed window is no longer a live closure"); + th_advance(t, 100); + CHECK_EQ(late_timeouts, 0, "timeout from the destroy callback dropped"); + CHECK_EQ(late_works, 0, "work from the destroy callback dropped"); + th_destroy(t); +} + +#if defined(CONFIG_WIDGETS) +/* + * A widget callback repaints on Destroy (e.g. it updates a label). The + * toplevel being torn down must not queue paint work that outlives it. + */ +static int destroy_repaints; + +static twin_dispatch_result_t repaint_on_destroy(twin_widget_t *widget, + twin_event_t *event, + void *data) +{ + (void) data; + if (event->kind == TwinEventDestroy) { + destroy_repaints++; + twin_widget_queue_paint(widget); + } + return TwinDispatchContinue; +} + +static void test_toplevel_destroy_repaint(void) +{ + th_t *t = th_create(W, H); + twin_toplevel_t *top = twin_toplevel_create(t->screen, 0, 0, W, H, "t"); + twin_label_t *label = twin_label_create( + &top->box, "label", 0xff000000, twin_int_to_fixed(12), TwinStyleRoman); + twin_widget_set_callback(&label->widget, repaint_on_destroy, NULL); + twin_toplevel_show(top); + th_frame(t); /* painted: no paint pending anywhere */ + + void *addr = top; + destroy_repaints = 0; + twin_window_destroy(top->box.widget.window); + CHECK_EQ(destroy_repaints, 1, "callback saw the destroy"); + CHECK(!_twin_closure_is_valid(addr), + "no paint work registered on the freed toplevel"); + th_frame(t); + th_destroy(t); +} +#endif + +#if defined(CONFIG_WM_DESKTOP) +/* + * Destroying a window that was never shown must leave focus alone: here a + * popup holds it, and no other window may be activated in its place. + */ +static void test_desktop_destroy_unshown(void) +{ + th_t *t = th_create(W, H); + probe_t pa = {0}, pp = {0}, pb = {0}; + twin_window_t *a = probe_window(t, &pa, TwinWindowApplication, 10, 10); + twin_window_show(a); + twin_window_t *popup = probe_window(t, &pp, TwinWindowPlain, 80, 60); + twin_window_show(popup); + twin_screen_set_active(t->screen, popup->pixmap); + th_settle(t, 500); + CHECK(t->screen->active == popup->pixmap, "popup has focus"); + + twin_window_t *b = probe_window(t, &pb, TwinWindowApplication, 30, 30); + int activates = pa.activates, deactivates = pp.deactivates; + twin_window_destroy(b); + th_settle(t, 500); + CHECK(t->screen->active == popup->pixmap, + "focus stays where it was after destroying a never-shown window"); + CHECK_EQ(pa.activates, activates, "no Activate sent to another window"); + CHECK_EQ(pp.deactivates, deactivates, "no Deactivate sent to the popup"); + + twin_window_destroy(popup); + twin_window_destroy(a); + th_destroy(t); +} +#endif + +#if defined(CONFIG_WM_MOBILE) +/* + * Under the mobile policy a destroyed app's pixels are adopted to animate + * the pop, leaving window->pixmap NULL while the destroy callback runs. + * Re-laying out every window from that callback must skip the window, not + * crash. + */ +static void relayout_on_destroy(twin_window_t *w) +{ + CHECK(!w->pixmap, "pixels adopted for the pop animation"); + twin_screen_set_inset(w->screen, 30); +} + +static void test_relayout_from_destroy(void) +{ + th_t *t = th_create(W, H); + probe_t pa = {0}, pb = {0}; + twin_window_t *a = probe_window(t, &pa, TwinWindowApplication, 0, 0); + twin_window_show(a); + th_settle(t, 1000); + twin_window_t *b = probe_window(t, &pb, TwinWindowApplication, 0, 0); + twin_window_show(b); + th_settle(t, 1000); + CHECK(b->pixmap->screen && b->pixmap->x == 0, "second app on top"); + + b->destroy = relayout_on_destroy; + twin_window_destroy(b); + th_settle(t, 1000); + CHECK(a->pixmap && a->pixmap->screen && a->pixmap->height == H - 30, + "remaining app shown above the inset"); + CHECK_EQ(th_damage_check(t), 0, "screen consistent after the relayout"); + + twin_window_destroy(a); + th_destroy(t); +} +#endif + +/* + * A tween that finishes itself from its own step callback still promises + * the final TWIN_FIXED_ONE frame, delivered once, and then stops. + */ +typedef struct { + twin_tween_t *self; + int calls, ones; + twin_fixed_t last; +} self_finish_t; + +static bool self_finish(twin_fixed_t value, void *closure) +{ + self_finish_t *s = closure; + s->calls++; + s->last = value; + if (value == TWIN_FIXED_ONE) + s->ones++; + if (s->calls == 2) + twin_tween_finish(s->self); + return true; +} + +static void test_tween_finish_from_proc(void) +{ + th_t *t = th_create(W, H); + self_finish_t s = {0}; + s.self = twin_tween_start(1000, TwinEaseLinear, self_finish, &s); + th_advance(t, 100); + CHECK_EQ(s.last, TWIN_FIXED_ONE, "final frame delivered"); + CHECK_EQ(s.ones, 1, "final frame delivered exactly once"); + CHECK_EQ(s.calls, 3, "no frames after finishing"); + int calls = s.calls; + th_advance(t, 1000); + CHECK_EQ(s.calls, calls, "finished tween is gone"); + th_destroy(t); +} + +/* + * A path whose cache key collides with an entry of another size must be + * rasterized uncached: inserting a duplicate key would create entries no + * lookup can reach, and repeat the waste on every draw. + * + * The collision is forged by planting an entry under the path's key. The + * key formula is mirrored from _twin_path_key(); a sanity check proves the + * mirror matches, so a change there fails loudly instead of passing. + */ +static uint64_t mirror_path_key(const twin_path_t *path, const twin_rect_t *b) +{ + uint64_t h = TWIN_HASH_INIT ^ 0x50; + twin_sfixed_t ox = twin_int_to_sfixed(b->left); + twin_sfixed_t oy = twin_int_to_sfixed(b->top); + for (int i = 0; i < path->npoints; i++) { + twin_sfixed_t xy[2] = {(twin_sfixed_t) (path->points[i].x - ox), + (twin_sfixed_t) (path->points[i].y - oy)}; + h = _twin_hash(h, xy, sizeof(xy)); + } + h = _twin_hash(h, path->sublen, sizeof(int) * path->nsublen); + h = _twin_hash(h, &path->npoints, sizeof(path->npoints)); + return h; +} + +static void test_cache_collision(void) +{ + if (!_twin_cache_enabled()) { + printf("lifetime: cache collision test skipped (cache disabled)\n"); + return; + } + twin_pixmap_t *ref = twin_pixmap_create(TWIN_ARGB32, 48, 48); + twin_pixmap_t *px = twin_pixmap_create(TWIN_ARGB32, 48, 48); + twin_fill(ref, 0xffffffff, TWIN_SOURCE, 0, 0, 48, 48); + twin_fill(px, 0xffffffff, TWIN_SOURCE, 0, 0, 48, 48); + twin_path_t *path = twin_path_create(); + twin_path_circle(path, twin_int_to_fixed(20), twin_int_to_fixed(20), + twin_int_to_fixed(9)); + twin_rect_t b; + twin_path_bounds(path, &b); + twin_coord_t w = b.right - b.left, h = b.bottom - b.top; + uint64_t key = mirror_path_key(path, &b); + + twin_cache_flush(); + twin_paint_path(ref, 0xff000000, path); + twin_cache_entry_t *e = _twin_cache_lookup(key); + CHECK(e && e->width == w && e->height == h, + "mirrored key finds the cached mask"); + + twin_cache_flush(); + CHECK(_twin_cache_insert(key, w + 3, h) != NULL, "collision planted"); + twin_cache_stats_t s0, s1; + twin_cache_get_stats(&s0); + twin_paint_path(px, 0xff000000, path); + twin_paint_path(px, 0xff000000, path); + twin_cache_get_stats(&s1); + CHECK_EQ(s1.entries, s0.entries, "no duplicate key inserted"); + CHECK_EQ(s1.bytes, s0.bytes, "no cache bytes wasted"); + + /* The uncached raster matches the cached one: paint the reference a + * second time too, so both have the same edges blended twice */ + twin_paint_path(ref, 0xff000000, path); + bool same = true; + for (int y = 0; y < 48 && same; y++) + same = !memcmp(twin_pixmap_pointer(ref, 0, y).v, + twin_pixmap_pointer(px, 0, y).v, 48 * 4); + CHECK(same, "uncached raster equals the cached one"); + + twin_path_destroy(path); + twin_pixmap_destroy(px); + twin_pixmap_destroy(ref); + twin_cache_flush(); +} + +/* + * Destroying a screen in the middle of whatever its window manager is doing + * (a push transition on a phone, a drag on a desktop) returns every byte, + * and destroy callbacks that act on a sibling during the teardown (show, + * hide, reconfigure, activate) reach nothing that is gone. + */ +static twin_window_t *teardown_sibling; +static int teardown_pokes; + +static void poke_sibling(twin_window_t *w) +{ + teardown_pokes++; + twin_window_t *s = teardown_sibling; + if (!s || s == w) + return; + twin_window_show(s); + twin_window_hide(s); + twin_window_configure(s, TwinWindowApplication, 0, 0, 50, 50); + twin_window_activate(s); + twin_window_show(s); +} + +static void forget_sibling(twin_window_t *w) +{ + if (teardown_sibling == w) + teardown_sibling = NULL; +} + +static void test_screen_teardown(void) +{ + twin_cache_flush(); + size_t before = th_heap_raw(); + th_t *t = th_create(W, H); + probe_t pa = {0}, pb = {0}, pc = {0}; + twin_window_t *a = probe_window(t, &pa, TwinWindowApplication, 0, 0); + twin_window_show(a); + th_settle(t, 500); + twin_window_t *b = probe_window(t, &pb, TwinWindowApplication, 20, 20); + twin_window_show(b); + /* Left unmapped: shown, it would end the push on a phone and take the + * press on a desktop. It is still destroyed as a sibling. */ + twin_window_t *c = probe_window(t, &pc, TwinWindowPlain, 5, 5); + /* Mid push on a phone; press and move on b's top edge (its title bar on + * a desktop) to be mid drag there */ + th_advance(t, 48); + th_press(t, b->pixmap->x + 30, b->pixmap->y + 4); + th_move(t, b->pixmap->x + 50, b->pixmap->y + 14); + + a->destroy = poke_sibling; + b->destroy = poke_sibling; + c->destroy = forget_sibling; + teardown_sibling = c; + teardown_pokes = 0; + th_destroy(t); + twin_cache_flush(); + CHECK_EQ(teardown_pokes, 2, "destroy callbacks ran once each"); + CHECK(!teardown_sibling, "the sibling went too"); +#if defined(CONFIG_MEMORY_STATS) + CHECK(th_heap_raw() <= before, "teardown returns every byte (%zu > %zu)", + th_heap_raw(), before); +#else + (void) before; +#endif +} + +int main(void) +{ + test_window_destroy_schedules(); +#if defined(CONFIG_WIDGETS) + test_toplevel_destroy_repaint(); +#endif +#if defined(CONFIG_WM_DESKTOP) + test_desktop_destroy_unshown(); +#endif +#if defined(CONFIG_WM_MOBILE) + test_relayout_from_destroy(); +#endif + test_tween_finish_from_proc(); + test_screen_teardown(); + test_cache_collision(); + twin_cache_flush(); + return th_report("lifetime"); +} diff --git a/tests/test-wm.c b/tests/test-wm.c new file mode 100644 index 00000000..cd2a1ccf --- /dev/null +++ b/tests/test-wm.c @@ -0,0 +1,1089 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Window manager tests: placement and input under the policy this build + * has, mobile transitions and eviction. Every step checks that incremental + * scan-out matches a from-scratch recomposite. + */ + +#include +#include + +#include "harness.h" + +#define W 240 +#define H 320 + +#define TOP_BAR 20 +#define BOTTOM_BAR 32 + +typedef struct { + twin_argb32_t color; + int draws, closes, presses, destroyed, keys, ups, backs; + bool refuse_close; + bool handle_back; /* consume TwinEventBack (in-app history) */ + bool pass_presses; /* leave presses to the window manager */ + twin_coord_t last_x, last_y; +} app_t; + +static void app_draw(twin_window_t *w) +{ + app_t *a = w->client_data; + a->draws++; + twin_coord_t cw = w->client.right - w->client.left; + twin_coord_t ch = w->client.bottom - w->client.top; + twin_fill(w->pixmap, a->color, TWIN_SOURCE, 0, 0, cw, ch); + twin_fill(w->pixmap, 0xff000000, TWIN_SOURCE, 4, 4, 14, 14); +} + +static bool app_event(twin_window_t *w, twin_event_t *ev) +{ + app_t *a = w->client_data; + switch (ev->kind) { + case TwinEventClose: + a->closes++; + return a->refuse_close; + case TwinEventBack: + a->backs++; + return a->handle_back; + case TwinEventButtonDown: + a->presses++; + a->last_x = ev->u.pointer.x; + a->last_y = ev->u.pointer.y; + return !a->pass_presses; + case TwinEventButtonUp: + a->ups++; + return false; + case TwinEventUcs4: + if (ev->u.ucs4.ucs4 == 27) + return false; /* Escape is left to the window manager */ + a->keys++; + return true; + default: + return false; + } +} + +static void app_destroy(twin_window_t *w) +{ + app_t *a = w->client_data; + a->destroyed++; +} + +static twin_window_t *mk(th_t *t, + app_t *a, + twin_window_style_t style, + twin_coord_t x, + twin_coord_t y, + twin_coord_t w, + twin_coord_t h) +{ + twin_window_t *win = + twin_window_create(t->screen, TWIN_ARGB32, style, x, y, w, h); + win->client_data = a; + win->draw = app_draw; + win->event = app_event; + win->destroy = app_destroy; + twin_window_set_opaque(win, true); + twin_window_show(win); + twin_window_queue_paint(win); + return win; +} + +static bool shown(const twin_window_t *w) +{ + return w->pixmap && w->pixmap->screen && !_twin_pixmap_is_stub(w->pixmap); +} + +#define CONSISTENT(t, what) CHECK_EQ(th_damage_check(t), 0, what) + +static void test_none(void) +{ +#if defined(CONFIG_WM_NONE) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}, p = {.color = 0xff3333cc}; + twin_window_t *app = mk(t, &a, TwinWindowApplication, 30, 40, 100, 100); + twin_window_t *pop = mk(t, &p, TwinWindowPlain, 30, 40, 50, 60); + th_frame(t); + + CHECK(app->pixmap->x == 0 && app->pixmap->width == W && + app->pixmap->height == H, + "application covers the screen without a WM"); + CHECK(pop->pixmap->x == 30 && pop->pixmap->y == 40 && + pop->pixmap->width == 50, + "plain window maps where requested"); + CHECK(app->client.top == 0, "no decorations"); + CHECK(th_pixel(t, 100, 100) == 0xffcc3333, "app pixels on screen"); + CHECK(t->screen->active == pop->pixmap, + "the newest mapped window takes the keyboard"); + + th_tap(t, 35, 45); + CHECK(p.presses == 1 && p.last_x == 5 && p.last_y == 5, + "plain window gets client coordinates (%d,%d)", p.last_x, p.last_y); + th_tap(t, 200, 300); + CHECK(a.presses == 1 && a.last_x == 200, "app gets its own presses"); + CHECK(t->screen->active == app->pixmap, "press focuses the window"); + + twin_window_close(pop); + CHECK(p.closes == 1 && p.destroyed == 1, "close destroys by default"); + th_frame(t); + CONSISTENT(t, "none: after close"); + twin_window_destroy(app); + th_destroy(t); +#endif +} + +static void test_desktop(void) +{ +#if defined(CONFIG_WM_DESKTOP) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff33cc33}; + twin_window_t *wa = mk(t, &a, TwinWindowApplication, 10, 10, 120, 100); + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 60, 80, 120, 100); + th_frame(t); + + CHECK(wa->client.top > 0, "desktop decorates application windows"); + CHECK(wa->pixmap->x == 10 && wa->pixmap->y == 10, "frame at request"); + 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"); +#endif + CONSISTENT(t, "desktop: initial"); + + /* Click in a's client area raises and activates it */ + th_tap(t, 20, 10 + wa->client.top + 10); + CHECK(a.presses == 1 && a.last_x == 10 && a.last_y == 10, + "client coordinates exclude the title (%d,%d)", a.last_x, a.last_y); + CHECK(wa->active && !wb->active, "click activates"); + CHECK(t->screen->top == wa->pixmap, "click raises"); + CONSISTENT(t, "desktop: raise"); + + /* Drag by the title bar */ + th_drag(t, 50, 15, 70, 45, 4); + CHECK(wa->pixmap->x == 30 && wa->pixmap->y == 40, + "title drag moves the window (%d,%d)", wa->pixmap->x, wa->pixmap->y); + CONSISTENT(t, "desktop: drag"); + + /* Refused close keeps the window */ + a.refuse_close = true; + twin_window_close(wa); + CHECK(a.closes == 1 && a.destroyed == 0, "refused close keeps the window"); + a.refuse_close = false; + twin_window_close(wa); + CHECK(a.destroyed == 1, "accepted close destroys"); + th_frame(t); + CHECK(wb->active, "focus moves to the remaining window"); + CONSISTENT(t, "desktop: close"); + twin_window_destroy(wb); + th_destroy(t); +#endif +} + +#if defined(CONFIG_WM_DESKTOP) +/* Tap along @w's title bar from the right edge leftwards until @found holds; + * returns the x that did it, or -1. Close requests must be refused. */ +static twin_coord_t tap_title_until(th_t *t, + twin_window_t *w, + bool (*found)(const twin_window_t *)) +{ + twin_coord_t left = w->pixmap->x, y = w->pixmap->y + w->client.top / 2; + for (twin_coord_t x = left + w->pixmap->width - 2; x > left; x--) { + th_advance(t, 400); /* never a double-click */ + th_tap(t, x, y); + if (found(w)) + return x; + } + return -1; +} + +static bool is_iconified(const twin_window_t *w) +{ + return w->pixmap->scan_height < w->pixmap->height; +} + +static bool is_restored(const twin_window_t *w) +{ + return w->pixmap->scan_height == w->pixmap->height; +} +#endif + +/* Iconify shows only the title bar: the rest neither scans out nor takes + * the pointer, and restoring brings the retained pixels back */ +static void test_desktop_iconify(void) +{ +#if defined(CONFIG_WM_DESKTOP) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333, .refuse_close = true}; + app_t b = {.color = 0xff33cc33, .refuse_close = true}; + /* Lower and offset, so a's title bar stays clear when b is raised */ + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 20, 60, 160, 140); + twin_window_t *wa = mk(t, &a, TwinWindowApplication, 20, 30, 160, 140); + th_frame(t); + twin_coord_t below_y = wa->pixmap->y + wa->client.top + 40; + CHECK(th_pixel(t, 100, below_y) == 0xffcc3333, "top window covers"); + + CHECK(tap_title_until(t, wa, is_iconified) > 0, "iconify button found"); + th_frame(t); + CHECK_EQ(wa->pixmap->scan_height, wa->client.top, + "iconified down to the title bar"); + CHECK(th_pixel(t, 100, below_y) == 0xff33cc33, + "window below shows under an iconified one"); + CONSISTENT(t, "desktop: iconified"); + + int presses = b.presses; + th_tap(t, 100, below_y); + CHECK_EQ(b.presses, presses + 1, "click below the title falls through"); + CHECK(t->screen->top == wb->pixmap, "and raises the window below"); + + /* Bring the iconified window back on top, then restore it */ + th_tap(t, wa->pixmap->x + 30, wa->pixmap->y + wa->client.top / 2); + CHECK(tap_title_until(t, wa, is_restored) > 0, "restore button found"); + th_frame(t); + CHECK(th_pixel(t, 100, below_y) == 0xffcc3333, "restored window covers"); + presses = a.presses; + th_tap(t, 100, below_y); + CHECK_EQ(a.presses, presses + 1, "restored client takes the click"); + CONSISTENT(t, "desktop: restored"); + + twin_window_destroy(wa); + twin_window_destroy(wb); + th_destroy(t); +#endif +} + +/* + * Restoring an evicted surface keeps everything that describes the window + * on screen: focus, pointer grab, opacity, shadow, the shown rows and the + * update-disable count all move to the fresh pixmap. + */ +static void test_restore_handoff(void) +{ + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}; + twin_window_t *w = mk(t, &a, TwinWindowPlain, 20, 20, 100, 80); + th_frame(t); + + _twin_window_evict(w); + twin_pixmap_t *stub = w->pixmap; + CHECK(_twin_pixmap_is_stub(stub) && stub->screen, + "evicted surface keeps its place in the stack"); + t->screen->active = stub; + t->screen->target = stub; + twin_pixmap_set_opacity(stub, 0x80); + twin_pixmap_set_shadow(stub, 2, 3, 6, 0x90); + stub->scan_height = 50; + twin_pixmap_disable_update(stub); + + CHECK(_twin_window_restore(w), "restore"); + twin_pixmap_t *p = w->pixmap; + CHECK(!_twin_pixmap_is_stub(p) && p->screen, "restored and shown"); + CHECK(t->screen->active == p, "focus follows the restored surface"); + CHECK(t->screen->target == p, "pointer target follows"); + CHECK_EQ(p->opacity, 0x80, "opacity carried"); +#if defined(CONFIG_DROP_SHADOW) + CHECK_EQ(p->shadow.blur, 6, "shadow carried"); +#endif + CHECK_EQ(p->scan_height, 50, "shown rows carried"); + CHECK_EQ(p->disable, 1, "update disable carried"); + twin_pixmap_enable_update(p); + t->screen->target = NULL; + t->screen->clicklock = false; + th_frame(t); + CONSISTENT(t, "restored surface consistent"); + + twin_window_destroy(w); + th_destroy(t); +} + +#if defined(CONFIG_WM_MOBILE) +typedef struct { + th_t *t; + app_t top, bottom; + twin_window_t *bar_top, *bar_bottom; +} phone_t; + +static void phone_init(phone_t *ph) +{ + memset(ph, 0, sizeof(*ph)); + ph->t = th_create(W, H); + ph->top.color = 0xff202020; + ph->bottom.color = 0xff404040; + ph->bar_top = mk(ph->t, &ph->top, TwinWindowPlain, 0, 0, W, TOP_BAR); + ph->bar_bottom = + mk(ph->t, &ph->bottom, TwinWindowPlain, 0, 0, W, BOTTOM_BAR); + twin_screen_set_bars(ph->t->screen, ph->bar_top, ph->bar_bottom); + th_frame(ph->t); +} + +static void phone_fini(phone_t *ph) +{ + twin_window_destroy(ph->bar_top); + twin_window_destroy(ph->bar_bottom); + th_destroy(ph->t); +} + +static twin_window_t *launch(phone_t *ph, app_t *a) +{ + twin_rect_t work = twin_screen_work_area(ph->t->screen); + return mk(ph->t, a, TwinWindowApplication, work.left, work.top, + work.right - work.left, work.bottom - work.top); +} + +static void test_mobile_layout(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + twin_rect_t work = twin_screen_work_area(t->screen); + CHECK(work.top == TOP_BAR && work.bottom == H - BOTTOM_BAR, + "work area between the bars"); + CHECK(ph.bar_bottom->pixmap->y == H - BOTTOM_BAR, "bottom bar pinned"); + + app_t a = {.color = 0xffcc3333}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + CHECK(wa->pixmap->y == TOP_BAR && + wa->pixmap->height == H - TOP_BAR - BOTTOM_BAR, + "app fills the work area"); + CHECK(th_pixel(t, 100, 100) == 0xffcc3333, "app visible"); + CHECK(th_pixel(t, 100, 5) == 0xff202020, "status bar above it"); + CHECK(t->screen->active == wa->pixmap, "top app has focus"); + + th_tap(t, 30, TOP_BAR + 30); + CHECK(a.presses == 1 && a.last_y == 30, "app gets work-area coordinates"); + CONSISTENT(t, "mobile: layout"); + twin_window_destroy(wa); + phone_fini(&ph); +} + +static void test_mobile_push_pop(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff33cc33}; + + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); +#if defined(CONFIG_WM_MOBILE_EVICT) + size_t one_app = th_heap_net(t); +#endif + + twin_window_t *wb = launch(&ph, &b); + th_advance(t, 64); + CHECK(wb->pixmap->x > 0 && wb->pixmap->x < W, + "push: incoming app mid-slide (x=%d)", wb->pixmap->x); + CHECK(wa->pixmap->x < 0, "push: outgoing app parallaxes left"); + CHECK(wa->pixmap->opacity < 0xff, "push: outgoing app dims"); +#if defined(CONFIG_DROP_SHADOW) + CHECK(wb->pixmap->shadow.blur > 0, "push: incoming app casts a shadow"); +#endif + CONSISTENT(t, "mobile: mid push"); + + /* Taps are ignored while the transition runs */ + int presses = b.presses; + th_tap(t, 200, 150); + CHECK_EQ(b.presses, presses, "no input during transitions"); + + th_settle(t, 1000); + CHECK(shown(wb) && wb->pixmap->x == 0, "push settles at the origin"); + CHECK(!wa->pixmap->screen, "covered app hidden"); +#if defined(CONFIG_WM_MOBILE_EVICT) + CHECK(_twin_pixmap_is_stub(wa->pixmap), "covered app evicted"); + size_t two_apps = th_heap_net(t); + CHECK(two_apps < one_app + 4096, "eviction keeps one surface (%zu -> %zu)", + one_app, two_apps); +#endif + CHECK(th_pixel(t, 100, 100) == 0xff33cc33, "new app on screen"); + CONSISTENT(t, "mobile: after push"); + + /* Pop: destroy the top app; its pixels leave while 'a' repaints */ + int draws = a.draws; + twin_window_destroy(wb); + CHECK_EQ(b.destroyed, 1, "app state freed immediately"); + th_advance(t, 64); +#if defined(CONFIG_WM_MOBILE_EVICT) + CHECK(a.draws > draws, "revealed app repainted after eviction"); +#else + CHECK_EQ(a.draws, draws, "retained app revealed without a repaint"); +#endif + CHECK(wa->pixmap->x < 0 && wa->pixmap->x > -W, "pop: revealed app slides"); + CONSISTENT(t, "mobile: mid pop"); + th_settle(t, 1000); + CHECK(shown(wa) && wa->pixmap->x == 0 && wa->pixmap->opacity == 0xff, + "pop settles"); + CHECK(th_pixel(t, 100, 100) == 0xffcc3333, "previous app back"); + CHECK(t->screen->active == wa->pixmap, "focus returns"); + CONSISTENT(t, "mobile: after pop"); + + twin_window_destroy(wa); + th_settle(t, 1000); + phone_fini(&ph); +} + +static void test_mobile_swipe(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff33cc33}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + twin_window_t *wb = launch(&ph, &b); + th_settle(t, 1000); + + /* Short swipe: springs back */ + th_press(t, 3, 150); + th_move(t, 30, 150); + CHECK(wb->pixmap->x == 27, "swipe tracks the finger (x=%d)", wb->pixmap->x); + CHECK(shown(wa), "app below revealed during the swipe"); + CONSISTENT(t, "mobile: mid swipe"); + th_release(t, 30, 150); + th_settle(t, 1000); + CHECK(b.closes == 0 && shown(wb) && wb->pixmap->x == 0, + "short swipe cancels"); + CHECK(b.presses == 0, "edge swipe never reached the app"); + + /* Long swipe closes the top app */ + th_drag(t, 3, 150, 180, 150, 8); + th_settle(t, 1000); + CHECK(b.closes == 1 && b.destroyed == 1, "long swipe closes"); + CHECK(shown(wa) && wa->pixmap->x == 0, "swipe pop settles"); + CONSISTENT(t, "mobile: after swipe"); + + /* An app that refuses to close slides back */ + app_t c = {.color = 0xff3333cc, .refuse_close = true}; + twin_window_t *wc = launch(&ph, &c); + th_settle(t, 1000); + th_drag(t, 3, 150, 200, 150, 8); + th_settle(t, 1000); + CHECK(c.closes == 1 && c.destroyed == 0 && shown(wc) && wc->pixmap->x == 0, + "refused swipe-close springs back"); + CONSISTENT(t, "mobile: refused swipe"); + + /* Single app: no swipe, the press reaches the app */ + twin_window_destroy(wc); + th_settle(t, 1000); + th_tap(t, 3, 150); + CHECK(a.presses == 1, "edge press reaches a lone app"); + twin_window_destroy(wa); + phone_fini(&ph); +} + +static void test_mobile_fullscreen(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t home = {.color = 0xffcccccc}, lock = {.color = 0xff000040}; + twin_window_t *wh = launch(&ph, &home); + twin_window_t *wl = mk(t, &lock, TwinWindowFullscreen, 0, 0, W, H); + th_settle(t, 1000); + CHECK(wl->pixmap->y == 0 && wl->pixmap->height == H, "lock covers all"); + CHECK(th_pixel(t, 100, 5) == 0xff000040, "lock above the status bar"); + CHECK(t->screen->active == wl->pixmap, "lock has focus"); + CHECK(!wh->pixmap->screen && !ph.bar_top->pixmap->screen, + "app and bars hidden under an opaque full-screen window"); +#if defined(CONFIG_WM_MOBILE_EVICT) + CHECK(_twin_pixmap_is_stub(wh->pixmap), "covered app released"); +#endif + CONSISTENT(t, "mobile: locked"); + th_tap(t, 3, 150); + CHECK(lock.presses == 1 && home.presses == 0, "no edge swipe under lock"); + + twin_window_destroy(wl); + th_advance(t, 80); + CONSISTENT(t, "mobile: mid dismiss"); + th_settle(t, 1000); + CHECK(th_pixel(t, 100, 100) == 0xffcccccc, "home after unlock"); + CHECK(shown(ph.bar_top) && shown(ph.bar_bottom), "bars back after unlock"); + CHECK(t->screen->active == wh->pixmap, "home focused"); + twin_window_destroy(wh); + phone_fini(&ph); +} + +/* Popping while an opaque full-screen window covers everything must not + * link the departing pixels above a released (off-screen) window */ +static void test_mobile_pop_under_fullscreen(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffaa0000}, b = {.color = 0xff00aa00}, + f = {.color = 0xff0000aa}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + twin_window_t *wb = launch(&ph, &b); + th_settle(t, 1000); + twin_window_t *wf = mk(t, &f, TwinWindowFullscreen, 0, 0, W, H); + twin_window_set_opaque(wf, false); + th_settle(t, 1000); + twin_window_set_opaque(wf, true); /* now covers everything */ + twin_window_destroy(wb); + bool reachable = false; + for (twin_pixmap_t *p = t->screen->top; p; p = p->down) + if (p == wf->pixmap) + reachable = true; + CHECK(reachable, "full-screen window still in the display list"); + th_settle(t, 1000); + CONSISTENT(t, "pop under full-screen"); + twin_window_hide(wf); + th_settle(t, 1000); + CHECK(shown(wa), "app revealed after the cover goes away"); + CONSISTENT(t, "uncovered after pop"); + + /* Style change of a mapped window re-runs the policy */ + twin_window_configure(wa, TwinWindowFullscreen, 0, 0, W, H); + th_settle(t, 1000); + CHECK(wa->pixmap->y == 0 && wa->pixmap->height == H, + "restyled window covers the screen"); + CONSISTENT(t, "after restyle"); + twin_window_destroy(wf); + twin_window_destroy(wa); + th_settle(t, 1000); + phone_fini(&ph); +} + +static void test_mobile_memory(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t apps[6]; + twin_window_t *win[6]; + size_t base = th_heap_net(t); + + for (int i = 0; i < 6; i++) { + apps[i] = (app_t) {.color = 0xff000000 | (0x2a * (i + 1))}; + win[i] = launch(&ph, &apps[i]); + th_settle(t, 1000); + } +#if defined(CONFIG_WM_MOBILE_EVICT) + size_t deep = th_heap_net(t); + size_t surface = (size_t) W * (H - TOP_BAR - BOTTOM_BAR) * 4; + CHECK(deep - base < surface + 6 * 1024, + "six stacked apps cost one surface (%zu bytes)", deep - base); +#endif + + /* Unwind bottom-up below the top, then pop the top once */ + for (int i = 1; i < 5; i++) + twin_window_destroy(win[i]); + twin_window_destroy(win[5]); + th_settle(t, 1000); + CHECK(shown(win[0]), "back to the root"); + CONSISTENT(t, "mobile: unwound"); + twin_window_destroy(win[0]); + th_settle(t, 1000); + CHECK(th_heap_net(t) <= base, "no leak after the stack unwinds (%zu > %zu)", + th_heap_net(t), base); + phone_fini(&ph); +} +#endif /* CONFIG_WM_MOBILE */ + +/* Without a window manager, hiding the focused window hands the keyboard + * to the topmost window still shown */ +static int self_destroyed; + +/* Destroys its own window from the event named by the app's color */ +static bool destroy_on(twin_window_t *w, twin_event_t *ev) +{ + app_t *a = w->client_data; + if ((twin_event_kind_t) a->color == ev->kind) { + self_destroyed++; + twin_window_destroy(w); + return false; + } + return false; +} + +/* Windows destroyed from their own callbacks while the screen is busy are + * freed only once it is idle; nothing touches them after */ +static void test_destroy_in_callbacks(void) +{ +#if defined(CONFIG_WM_NONE) + /* Focus follows a press on any window only without a window manager */ + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}; + app_t b = {.color = TwinEventActivate}; + twin_window_t *wa = mk(t, &a, TwinWindowPlain, 0, 0, 100, 100); + twin_window_t *wb = mk(t, &b, TwinWindowPlain, 120, 120, 100, 100); + wb->draw = NULL; + wb->destroy = NULL; + twin_screen_set_active(t->screen, wa->pixmap); + wb->event = destroy_on; + self_destroyed = 0; + size_t before = th_heap_net(t); + th_tap(t, 150, 150); /* focusing b runs its Activate handler */ + CHECK_EQ(self_destroyed, 1, "destroyed from Activate"); + /* A Close handler that destroys the window itself */ + app_t e = {.color = TwinEventClose}; + twin_window_t *we = mk(t, &e, TwinWindowPlain, 50, 50, 40, 40); + we->event = destroy_on; + we->destroy = NULL; + twin_window_close(we); + CHECK_EQ(self_destroyed, 2, "destroyed from Close, once"); + CHECK(t->screen->windows == wa && !t->screen->zombies, + "freed once the press is delivered"); +#if defined(CONFIG_MEMORY_STATS) + CHECK(th_heap_net(t) < before, "memory returned"); +#else + (void) before; +#endif + CONSISTENT(t, "none: destroyed from Activate"); + twin_window_destroy(wa); + th_destroy(t); +#endif + +#if defined(CONFIG_WM_MOBILE) + /* Configure during a relayout of every window */ + phone_t ph; + phone_init(&ph); + app_t c = {.color = 0xff3333cc}, d = {.color = TwinEventConfigure}; + twin_window_t *wc = launch(&ph, &c); + twin_window_t *wd = launch(&ph, &d); + th_settle(ph.t, 1000); + wd->event = destroy_on; + wd->destroy = NULL; + self_destroyed = 0; + twin_screen_set_inset(ph.t->screen, 40); + th_settle(ph.t, 1000); + CHECK_EQ(self_destroyed, 1, "destroyed from Configure"); + CHECK(shown(wc) && wc->pixmap->height == H - TOP_BAR - BOTTOM_BAR - 40, + "the rest of the relayout went on"); + CONSISTENT(ph.t, "mobile: destroyed from Configure"); + twin_window_destroy(wc); + phone_fini(&ph); +#endif +} + +static void test_none_focus(void) +{ +#if defined(CONFIG_WM_NONE) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff3333cc}; + twin_window_t *wa = mk(t, &a, TwinWindowPlain, 10, 10, 80, 80); + twin_window_t *wb = mk(t, &b, TwinWindowPlain, 100, 100, 80, 80); + th_frame(t); + CHECK(t->screen->active == wb->pixmap, "newest window focused"); + twin_window_hide(wb); + CHECK(t->screen->active == wa->pixmap, "focus falls back on hide"); + th_key(t, 'x'); + CHECK_EQ(a.keys, 1, "keys reach the remaining window"); + twin_window_destroy(wb); + twin_window_destroy(wa); + th_destroy(t); +#endif +} + +#if defined(CONFIG_WM_DESKTOP) +/* Title bar point clear of the buttons (the name sits there) */ +static void title_point(const twin_window_t *w, + twin_coord_t *x, + twin_coord_t *y) +{ + *x = w->pixmap->x + 40; + *y = w->pixmap->y + w->client.top / 2; +} + +static bool down_destroys(twin_window_t *w, twin_event_t *ev) +{ + if (ev->kind == TwinEventButtonDown) { + twin_window_destroy(w); + return false; /* gone, and the press is not claimed */ + } + return false; +} + +static twin_window_t *leave_victim; + +static bool leave_destroys(twin_window_t *w, twin_event_t *ev) +{ + if (ev->kind == TwinEventLeave && leave_victim == w) { + leave_victim = NULL; + app_t *a = w->client_data; + twin_window_destroy(w); + a->destroyed++; /* no destroy callback: count it here */ + return true; + } + return app_event(w, ev); +} +#endif + +static void test_desktop_focus(void) +{ +#if defined(CONFIG_WM_DESKTOP) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333, .refuse_close = true}; + app_t b = {.color = 0xff33cc33, .refuse_close = true}; + app_t c = {.color = 0xff3333cc, .refuse_close = true}; + twin_window_t *wa = mk(t, &a, TwinWindowApplication, 0, 0, 100, 60); + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 20, 100, 160, 100); + twin_window_t *wc = mk(t, &c, TwinWindowApplication, 20, 60, 160, 100); + th_frame(t); + CHECK(wc->active && t->screen->active == wc->pixmap, "newest active"); + + /* Closing a background window leaves the focus alone */ + twin_window_destroy(wa); + CHECK(wc->active && t->screen->active == wc->pixmap, + "unmapping an inactive window keeps the focus"); + + /* Iconify the focused window: the keyboard moves to one with content */ + CHECK(tap_title_until(t, wc, is_iconified) > 0, "iconify button found"); + CHECK(!wc->active && wb->active && t->screen->active == wb->pixmap, + "iconified window gives up the keyboard"); + th_key(t, 'k'); + CHECK(b.keys == 1 && c.keys == 0, "keys go to the visible window"); + + /* Clicking the iconified title raises it but leaves the keyboard */ + twin_coord_t x, y; + title_point(wc, &x, &y); + th_tap(t, x, y); + th_advance(t, 400); + CHECK(t->screen->top == wc->pixmap && t->screen->active == wb->pixmap, + "raised iconified window stays unfocused"); + + /* Explicit activation restores it */ + twin_window_activate(wc); + CHECK(is_restored(wc) && wc->active && t->screen->active == wc->pixmap, + "twin_window_activate restores and focuses"); + + /* Focus cycling walks the stack */ + twin_screen_focus_next(t->screen); + CHECK(wb->active && t->screen->top == wb->pixmap, "focus_next raises b"); + twin_screen_focus_next(t->screen); + CHECK(wc->active && t->screen->top == wc->pixmap, "and back to c"); + CONSISTENT(t, "desktop: focus"); + + twin_window_destroy(wc); + CHECK(wb->active, "closing the focused window hands focus on"); + twin_window_destroy(wb); + CHECK(t->screen->active == NULL, "no window, no focus"); + th_destroy(t); +#endif +} + +static void test_desktop_moves(void) +{ +#if defined(CONFIG_WM_DESKTOP) + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333, .pass_presses = true}; + app_t b = {.color = 0xff33cc33}; + twin_window_t *wa = mk(t, &a, TwinWindowApplication, 10, 10, 120, 100); + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 10, 10, 120, 100); + th_frame(t); + CHECK(wb->pixmap->x != wa->pixmap->x && wb->pixmap->y != wa->pixmap->y, + "a new window at an occupied spot cascades"); + twin_window_destroy(wb); + twin_coord_t x, y; + + /* Dragging never strands the title bar off screen */ + title_point(wa, &x, &y); + th_drag(t, x, y, -300, -300, 6); + CHECK(wa->pixmap->y >= 0 && wa->pixmap->x + wa->pixmap->width >= 32, + "title stays reachable (%d,%d)", wa->pixmap->x, wa->pixmap->y); + th_advance(t, 400); + title_point(wa, &x, &y); + if (x < 1) + x = wa->pixmap->x + wa->pixmap->width - 10; + th_drag(t, x, y, x + 400, y + 400, 6); + CHECK(wa->pixmap->x <= W - 32 && wa->pixmap->y <= H - 20, + "title stays on screen (%d,%d)", wa->pixmap->x, wa->pixmap->y); + CONSISTENT(t, "desktop: clamped drags"); + twin_window_configure(wa, TwinWindowApplication, 10, 10, 120, 100); + th_advance(t, 400); + + /* Double-click the title: maximize, and back */ + title_point(wa, &x, &y); + th_tap(t, x, y); + th_tap(t, x, y); + CHECK(wa->pixmap->x == 0 && wa->pixmap->y == 0 && wa->pixmap->width == W && + wa->pixmap->height == H, + "double-click maximizes (%d,%d %dx%d)", wa->pixmap->x, wa->pixmap->y, + wa->pixmap->width, wa->pixmap->height); + CONSISTENT(t, "desktop: maximized"); + th_advance(t, 400); + title_point(wa, &x, &y); + th_tap(t, x, y); + th_tap(t, x, y); + CHECK( + wa->pixmap->x == 10 && wa->pixmap->y == 10 && wa->pixmap->width == 120, + "second double-click restores (%d,%d %d)", wa->pixmap->x, wa->pixmap->y, + wa->pixmap->width); + th_advance(t, 400); + + /* Drop on the left edge: snap to the left half */ + title_point(wa, &x, &y); + th_drag(t, x, y, 0, 150, 6); + CHECK(wa->pixmap->x == 0 && wa->pixmap->width == W / 2 && + wa->pixmap->height == H, + "left-edge drop snaps (%d %dx%d)", wa->pixmap->x, wa->pixmap->width, + wa->pixmap->height); + /* Dragging a snapped window gives it back its size */ + th_advance(t, 400); + title_point(wa, &x, &y); + th_drag(t, x, y, x + 30, y + 40, 4); + CHECK_EQ(wa->pixmap->width, 120, "unsnapped by dragging"); + CONSISTENT(t, "desktop: snapped and back"); + + /* Unclaimed press in the bottom-right corner resizes */ + twin_coord_t cw = wa->client.right - wa->client.left; + twin_coord_t ch = wa->client.bottom - wa->client.top; + twin_coord_t gx = wa->pixmap->x + wa->client.right - 3; + twin_coord_t gy = wa->pixmap->y + wa->client.bottom - 3; + twin_coord_t px = wa->pixmap->x, py = wa->pixmap->y; + th_drag(t, gx, gy, gx + 20, gy + 10, 4); + CHECK_EQ(wa->client.right - wa->client.left, cw + 20, "resize width"); + CHECK_EQ(wa->client.bottom - wa->client.top, ch + 10, "resize height"); + CHECK(wa->pixmap->x == px && wa->pixmap->y == py, "resize keeps origin"); + th_drag(t, gx + 20, gy + 10, gx - 300, gy - 300, 4); + CHECK(wa->client.right - wa->client.left >= 48, "resize keeps a minimum"); + CONSISTENT(t, "desktop: resized"); + twin_window_configure(wa, TwinWindowApplication, 10, 10, 120, 100); + + /* A release over another window still ends the drag */ + app_t p = {.color = 0xff3333cc}; + twin_window_t *wp = mk(t, &p, TwinWindowPlain, 150, 200, 60, 60); + th_advance(t, 400); + title_point(wa, &x, &y); + th_drag(t, x, y, 170, 220, 6); + twin_coord_t dropped_x = wa->pixmap->x; + th_move(t, wa->pixmap->x + 10, wa->pixmap->y + 5); + CHECK_EQ(wa->pixmap->x, dropped_x, "no drag after a release elsewhere"); + CHECK(p.presses == 0, "popup never saw the drag"); + twin_window_destroy(wp); + + /* Clients that destroy themselves from their own handlers */ + app_t d = {.color = 0xff777777}; + twin_window_t *wd = mk(t, &d, TwinWindowApplication, 60, 60, 80, 80); + wd->event = down_destroys; + wd->destroy = NULL; + th_tap(t, wd->pixmap->x + 20, wd->pixmap->y + wd->client.top + 20); + CHECK(t->screen->windows == wa && !wa->wm_next, + "self-destroying press handled"); + app_t e = {.color = 0xff888888}; + twin_window_t *we = mk(t, &e, TwinWindowApplication, 60, 160, 80, 60); + we->event = leave_destroys; + we->destroy = NULL; + leave_victim = we; + th_move(t, we->pixmap->x + 20, we->pixmap->y + we->client.top + 10); + th_move(t, 5, 5); /* leave: we destroys itself */ + th_move(t, 200, 300); + CHECK(e.destroyed == 1, "destroyed from its own Leave"); + CONSISTENT(t, "desktop: self-destroyed"); + + twin_window_destroy(wa); + th_destroy(t); +#endif +} + +/* Rotation or a resized host: every policy re-places its windows */ +static void test_screen_resize(void) +{ +#if defined(CONFIG_WM_MOBILE) + { + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffcc3333}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + th_resize(t, H, W); + th_settle(t, 1000); + CHECK(wa->pixmap->width == H && + wa->pixmap->height == W - TOP_BAR - BOTTOM_BAR, + "app refits the rotated work area (%dx%d)", wa->pixmap->width, + wa->pixmap->height); + CHECK(ph.bar_bottom->pixmap->y == W - BOTTOM_BAR && + ph.bar_bottom->pixmap->width == H, + "bottom bar follows the new size"); + CONSISTENT(t, "mobile: rotated"); + twin_window_destroy(wa); + phone_fini(&ph); + } +#endif +#if defined(CONFIG_WM_DESKTOP) + { + th_t *t = th_create(W, H); + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff33cc33}; + twin_window_t *wa = mk(t, &a, TwinWindowApplication, 150, 200, 80, 80); + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 0, 0, 120, 60); + twin_coord_t x, y; + title_point(wb, &x, &y); + th_tap(t, x, y); + th_tap(t, x, y); /* maximize b */ + CHECK(wb->wm_maximized, "b maximized"); + th_resize(t, 200, 200); + th_settle(t, 1000); + twin_pixmap_t *p = wa->pixmap; + CHECK(p->x >= 0 && p->y >= 0 && p->x + p->width <= 200 && + p->y + p->height <= 200, + "window back on the smaller screen (%d,%d %dx%d)", p->x, p->y, + p->width, p->height); + CHECK(wb->pixmap->width == 200 && wb->pixmap->height == 200, + "maximized window refits (%dx%d)", wb->pixmap->width, + wb->pixmap->height); + CONSISTENT(t, "desktop: resized screen"); + twin_window_destroy(wa); + twin_window_destroy(wb); + th_destroy(t); + } +#endif +} + +#if defined(CONFIG_WM_MOBILE) +static void test_mobile_edge(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffcc3333}, b = {.color = 0xff33cc33}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + twin_window_t *wb = launch(&ph, &b); + th_settle(t, 1000); + + /* A tap at the edge is the app's, even with a stack to go back to */ + th_tap(t, 3, 150); + CHECK(b.presses == 1 && b.last_x == 3 && b.ups == 1, + "edge tap reaches the app (%d presses at %d)", b.presses, b.last_x); + CHECK(wb->pixmap->x == 0 && shown(wb), "no swipe for a tap"); + /* So does a vertical drag along the edge */ + th_drag(t, 3, 100, 5, 200, 6); + CHECK(b.presses == 2 && wb->pixmap->x == 0, + "vertical edge drag is the app's"); + CONSISTENT(t, "mobile: edge tap"); + + /* So does a press resting at the edge (long press) */ + th_press(t, 3, 150); + CHECK_EQ(b.presses, 2, "edge press held at first"); + th_advance(t, 200); + CHECK_EQ(b.presses, 3, "resting edge press handed to the app"); + th_release(t, 3, 150); + CHECK(wb->pixmap->x == 0, "long press is not a swipe"); + + /* An app with its own history takes the back gesture */ + b.handle_back = true; + th_drag(t, 3, 150, 200, 150, 8); + th_settle(t, 1000); + CHECK(b.backs == 1 && b.closes == 0 && shown(wb) && wb->pixmap->x == 0, + "handled back springs the window home"); + b.handle_back = false; + + /* Escape goes back when the app leaves it alone */ + th_key(t, 27); + th_settle(t, 1000); + CHECK(b.backs == 2 && b.destroyed == 1, "escape closes the top app"); + CHECK(shown(wa) && t->screen->active == wa->pixmap, "previous app back"); + th_key(t, 27); + th_settle(t, 1000); + CHECK(a.destroyed == 0 && shown(wa), "escape keeps the root app"); + + /* Keys follow a push at once; the press dropped mid-push stays dropped */ + app_t c = {.color = 0xff3333cc}; + twin_window_t *wc = launch(&ph, &c); + th_advance(t, 48); + CHECK(t->screen->active == wc->pixmap, "keyboard moves with the push"); + th_press(t, 120, 150); + th_settle(t, 1000); + th_release(t, 120, 150); + CHECK(c.presses == 0 && c.ups == 0 && a.ups == 0, + "release of a swallowed press is swallowed too"); + th_tap(t, 120, 150); + CHECK(c.presses == 1, "input back after the transition"); + CONSISTENT(t, "mobile: after edge tests"); + + twin_window_destroy(wc); + th_settle(t, 1000); + twin_window_destroy(wa); + phone_fini(&ph); +} + +static void test_mobile_bars(void) +{ + phone_t ph; + phone_init(&ph); + th_t *t = ph.t; + app_t a = {.color = 0xffcc3333}; + twin_window_t *wa = launch(&ph, &a); + th_settle(t, 1000); + twin_coord_t full = H - TOP_BAR - BOTTOM_BAR; + + twin_window_hide(ph.bar_bottom); + th_settle(t, 1000); + CHECK(twin_screen_work_area(t->screen).bottom == H && + wa->pixmap->height == H - TOP_BAR, + "a hidden bar gives its rows back (%d)", wa->pixmap->height); + CONSISTENT(t, "mobile: bar hidden"); + twin_window_show(ph.bar_bottom); + th_settle(t, 1000); + CHECK(wa->pixmap->height == full && shown(ph.bar_bottom), "bar back"); + + /* Keyboard inset */ + twin_screen_set_inset(t->screen, 60); + th_settle(t, 1000); + CHECK_EQ(wa->pixmap->height, full - 60, "inset shrinks the app"); + CONSISTENT(t, "mobile: inset"); + twin_screen_set_inset(t->screen, 0); + CHECK_EQ(wa->pixmap->height, full, "inset released"); + + /* Resizing a bar re-lays out the apps */ + twin_window_configure(ph.bar_bottom, TwinWindowPlain, 0, 0, W, 40); + th_settle(t, 1000); + CHECK(wa->pixmap->height == H - TOP_BAR - 40 && + ph.bar_bottom->pixmap->y == H - 40, + "taller bar, shorter app (%d)", wa->pixmap->height); + twin_window_configure(ph.bar_bottom, TwinWindowPlain, 0, 0, W, BOTTOM_BAR); + + /* An unregistered bar is unmapped, not left as a popup */ + twin_screen_set_bars(t->screen, ph.bar_top, NULL); + th_settle(t, 1000); + CHECK(!ph.bar_bottom->mapped && !ph.bar_bottom->pixmap->screen, + "replaced bar unmapped"); + CHECK_EQ(wa->pixmap->height, H - TOP_BAR, "its rows go to the app"); + CONSISTENT(t, "mobile: bar dropped"); + + /* A destroyed bar unregisters itself */ + twin_window_destroy(ph.bar_top); + ph.bar_top = NULL; + th_settle(t, 1000); + CHECK(t->screen->bar_top == NULL && wa->pixmap->y == 0 && + wa->pixmap->height == H, + "destroyed bar's space returns"); + CONSISTENT(t, "mobile: bar destroyed"); + + twin_window_destroy(wa); + twin_window_destroy(ph.bar_bottom); + th_destroy(t); +} +#endif + +int main(void) +{ + test_none(); + test_desktop(); + test_desktop_iconify(); + test_restore_handoff(); +#if defined(CONFIG_WM_MOBILE) + test_mobile_layout(); + test_mobile_push_pop(); + test_mobile_swipe(); + test_mobile_fullscreen(); + test_mobile_pop_under_fullscreen(); + test_mobile_memory(); + test_mobile_edge(); + test_mobile_bars(); +#endif + test_none_focus(); + test_destroy_in_callbacks(); + test_desktop_focus(); + test_desktop_moves(); + test_screen_resize(); + twin_cache_flush(); + return th_report("wm"); +} diff --git a/tools/headless-ctl.c b/tools/headless-ctl.c index bcd18e6e..d579c0af 100644 --- a/tools/headless-ctl.c +++ b/tools/headless-ctl.c @@ -252,6 +252,14 @@ static twin_headless_shm_t *connect_to_backend(int *fd_out, size_t *size_out) return NULL; } + /* Never read the header from an object too small to hold it */ + if (st.st_size < (off_t) sizeof(twin_headless_shm_t)) { + fprintf(stderr, "Shared memory too small (%lld bytes)\n", + (long long) st.st_size); + close(fd); + return NULL; + } + /* Map shared memory */ void *addr = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); @@ -279,6 +287,22 @@ static twin_headless_shm_t *connect_to_backend(int *fd_out, size_t *size_out) return NULL; } + /* The backend writes the header; still, every later framebuffer access + * trusts width * height, so it must fit what is actually mapped */ + if (shm->width <= 0 || shm->height <= 0 || + !twin_headless_shm_bytes((size_t) shm->width, (size_t) shm->height, + 1) || + twin_headless_shm_bytes((size_t) shm->width, (size_t) shm->height, 1) > + (size_t) st.st_size) { + fprintf(stderr, + "Invalid screen size %dx%d for %lld bytes of shared " + "memory\n", + shm->width, shm->height, (long long) st.st_size); + munmap(addr, st.st_size); + close(fd); + return NULL; + } + *fd_out = fd; *size_out = st.st_size; return shm; @@ -288,7 +312,12 @@ static int send_command(twin_headless_shm_t *shm, twin_headless_cmd_t cmd, const char *data) { - static uint32_t seq = 0; + /* Continue the backend's sequence: every headless-ctl run is a new + * process, so a counter of our own would restart at 1 and the backend + * would take the command for one it already answered */ + uint32_t seq = shm->command_seq + 1; + if (seq == 0) + seq = 1; shm->command = cmd; if (data) @@ -296,7 +325,7 @@ static int send_command(twin_headless_shm_t *shm, else shm->command_data[0] = '\0'; - shm->command_seq = ++seq; + shm->command_seq = seq; /* Wait for response (with timeout) */ int timeout = 30; /* 3 seconds total */ @@ -365,6 +394,89 @@ static int inject_mouse_event(twin_headless_shm_t *shm, } +/* + * Decode one UTF-8 character from @p into @c. Returns its length, or 0 for + * a malformed sequence: bad continuation bytes, overlong forms, UTF-16 + * surrogates and anything past U+10FFFF are all rejected. + */ +static int utf8_decode(const unsigned char *p, twin_ucs4_t *c) +{ + static const twin_ucs4_t min[] = {0, 0, 0x80, 0x800, 0x10000}; + int len; + twin_ucs4_t v; + + if (p[0] < 0x80) { + *c = p[0]; + return 1; + } + if ((p[0] & 0xe0) == 0xc0) { + len = 2; + v = p[0] & 0x1f; + } else if ((p[0] & 0xf0) == 0xe0) { + len = 3; + v = p[0] & 0x0f; + } else if ((p[0] & 0xf8) == 0xf0) { + len = 4; + v = p[0] & 0x07; + } else { + return 0; + } + for (int i = 1; i < len; i++) { + if ((p[i] & 0xc0) != 0x80) /* also stops at the terminating NUL */ + return 0; + v = (v << 6) | (p[i] & 0x3f); + } + if (v < min[len] || v > 0x10ffff || (v >= 0xd800 && v <= 0xdfff)) + return 0; + *c = v; + return len; +} + +/* + * Queue each UTF-8 character of @text as a UCS-4 key event. The whole + * string is validated first so malformed input delivers nothing; text + * longer than the queue waits for the backend to drain it. + */ +static int inject_text(twin_headless_shm_t *shm, const char *text) +{ + const unsigned char *p = (const unsigned char *) text; + size_t total = 0, sent = 0; + twin_ucs4_t c; + + while (*p) { + int len = utf8_decode(p, &c); + if (!len) { + fprintf(stderr, "Invalid UTF-8 sequence at byte %td\n", + (const char *) p - text); + return -1; + } + total++; + p += len; + } + + for (p = (const unsigned char *) text; *p; sent++) { + int timeout = 30; /* 3 seconds, as for commands */ + while (shm->event_write_idx - shm->event_read_idx >= + TWIN_HEADLESS_MAX_EVENTS && + timeout-- > 0) + usleep(100000); + if (timeout < 0) { + fprintf(stderr, + "Event queue full after %zu of %zu characters - backend " + "may be stalled\n", + sent, total); + return -1; + } + p += utf8_decode(p, &c); + twin_headless_event_t *evt = + &shm->events[shm->event_write_idx % TWIN_HEADLESS_MAX_EVENTS]; + evt->kind = TwinEventUcs4; + evt->u.ucs4.ucs4 = c; + shm->event_write_idx++; + } + return 0; +} + static void print_usage(const char *prog) { printf("Usage: %s [options] command\n", prog); @@ -373,6 +485,7 @@ static void print_usage(const char *prog) printf(" shot FILE Save screenshot to FILE.png\n"); printf(" shutdown Shutdown the backend\n"); printf(" mouse TYPE X Y [B] Inject mouse event (TYPE: move/down/up)\n"); + printf(" type TEXT Inject TEXT as character events\n"); printf(" monitor Monitor backend activity\n"); printf("\nOptions:\n"); printf(" -h, --help Show this help\n"); @@ -498,6 +611,15 @@ int main(int argc, char *argv[]) if (ret == 0) printf("Mouse event injected\n"); } + } else if (strcmp(command, "type") == 0) { + if (optind + 1 >= argc) { + fprintf(stderr, "Usage: type TEXT\n"); + ret = 1; + } else { + ret = inject_text(shm, argv[optind + 1]); + if (ret == 0) + printf("Text injected\n"); + } } else if (strcmp(command, "monitor") == 0) { printf("Monitoring backend activity (Ctrl+C to stop)...\n"); printf("Time Events Frames FPS Status\n"); diff --git a/tools/perf.c b/tools/perf.c index cdfb3d18..d75ae7b7 100644 --- a/tools/perf.c +++ b/tools/perf.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,8 @@ #include #endif -#include "twin.h" +/* White-box benchmark: drives a screen directly, as a backend does */ +#include "twin_private.h" #define TEST_PIX_WIDTH 1200 #define TEST_PIX_HEIGHT 800 @@ -302,6 +304,97 @@ static void run_large_tests(void) run_test_series("500x500 solid over", test_solid_over_argb32, 500, 500); } +/* + * Vector UI paths: glyph text and rounded rectangles with the coverage cache + * off and on, and a phone-like screen scan-out with occlusion culling off + * and on. + */ +static twin_path_t *vec_rrect; +static twin_screen_t *vec_screen; +static twin_pixmap_t *vec_layers[3]; + +static void test_text_14(void) +{ + twin_font_t *font = NULL; +#if defined(CONFIG_FONT_MONTSERRAT) + font = &twin_font_montserrat; +#endif + twin_text_draw(dst32, font, twin_int_to_fixed(14), twin_int_to_fixed(10), + twin_int_to_fixed(40), 0xff202020, "Messages 12:03 Hello"); +} + +static void test_rrect_44(void) +{ + twin_paint_path(dst32, 0xff3080c0, vec_rrect); +} + +static void test_scanout(void) +{ + twin_screen_damage(vec_screen, 0, 0, 240, 320); + twin_screen_update(vec_screen); +} + +static void discard_span(twin_coord_t left, + twin_coord_t top, + twin_coord_t right, + twin_argb32_t *pixels, + void *closure) +{ + (void) left; + (void) top; + (void) right; + (void) pixels; + (void) closure; +} + +static void run_vector_tests(void) +{ + twin_cache_stats_t stats; + twin_cache_get_stats(&stats); + /* Measure the cached case even when built with CACHE_SIZE=0 */ + size_t budget = stats.budget ? stats.budget : 24 * 1024; + + vec_rrect = twin_path_create(); + twin_path_rounded_rectangle(vec_rrect, twin_int_to_fixed(20), + twin_int_to_fixed(20), twin_int_to_fixed(44), + twin_int_to_fixed(44), twin_int_to_fixed(12), + twin_int_to_fixed(12)); + + twin_cache_set_budget(0); + run_test_series("text 20 glyphs 14px, uncached", test_text_14, 1, 1); + run_test_series("rounded rect 44x44, uncached", test_rrect_44, 1, 1); + twin_cache_set_budget(budget); + test_text_14(); + test_rrect_44(); + run_test_series("text 20 glyphs 14px, cached", test_text_14, 1, 1); + run_test_series("rounded rect 44x44, cached", test_rrect_44, 1, 1); + twin_cache_set_budget(stats.budget); + + /* App over a covered app, plus a status bar: a phone's steady state */ + vec_screen = twin_screen_create(240, 320, NULL, discard_span, NULL); + vec_layers[0] = twin_pixmap_create(TWIN_ARGB32, 240, 258); + vec_layers[1] = twin_pixmap_create(TWIN_ARGB32, 240, 258); + vec_layers[2] = twin_pixmap_create(TWIN_ARGB32, 240, 20); + for (int i = 0; i < 3; i++) { + twin_pixmap_t *p = vec_layers[i]; + twin_fill(p, 0xff406080 + i * 0x101010, TWIN_SOURCE, 0, 0, p->width, + p->height); + p->opaque_rect = (twin_rect_t) {0, p->width, 0, p->height}; + twin_pixmap_move(p, 0, i == 2 ? 0 : 20); + twin_pixmap_show(p, vec_screen, vec_screen->top); + } + _twin_debug |= TWIN_DEBUG_NO_OCCLUSION; + run_test_series("scan-out 240x320, 3 layers, no culling", test_scanout, 240, + 320); + _twin_debug &= ~TWIN_DEBUG_NO_OCCLUSION; + run_test_series("scan-out 240x320, 3 layers, culled", test_scanout, 240, + 320); + for (int i = 0; i < 3; i++) + twin_pixmap_destroy(vec_layers[i]); + twin_screen_destroy(vec_screen); + twin_path_destroy(vec_rrect); +} + /* Memory profiling mode */ /* Get memory usage statistics @@ -544,7 +637,7 @@ int main(void) printf("Sync time adjustment is %.4f msecs.\n\n", sync_time_adjustment); /* Create test pixmaps */ - src32 = twin_pixmap_from_file("assets/tux.png", TWIN_ARGB32); + src32 = twin_pixmap_from_file("assets/tux.png"); if (!src32) { /* Fallback: create a simple pixmap if file not found */ src32 = twin_pixmap_create(TWIN_ARGB32, 256, 256); @@ -568,11 +661,22 @@ int main(void) test_height = 1; test_argb32_source_argb32(); + /* MADO_PERF_ONLY=vector runs just the vector UI series */ + const char *only = getenv("MADO_PERF_ONLY"); + if (only && !strcmp(only, "vector")) { + run_vector_tests(); + twin_pixmap_destroy(src32); + twin_pixmap_destroy(dst32); + twin_pixmap_destroy(mask8); + return 0; + } + /* Run comprehensive performance test series */ run_basic_tests(); run_solid_tests(); run_alpha_tests(); run_large_tests(); + run_vector_tests(); /* Run memory profiling tests */ printf("\n"); diff --git a/tools/ttf/Makefile b/tools/ttf/Makefile index 8b61e2dc..7c1c32cc 100644 --- a/tools/ttf/Makefile +++ b/tools/ttf/Makefile @@ -1,7 +1,7 @@ TARGET = twin-ttf CFLAGS = $(shell pkg-config --cflags freetype2) -g -Wall -LIBS = $(shell pkg-config --libs freetype2) +LIBS = $(shell pkg-config --libs freetype2) -lm OBJS = \ twin-ttf.o diff --git a/tools/ttf/twin-ttf.c b/tools/ttf/twin-ttf.c index 54f4c422..bd0bbb4a 100644 --- a/tools/ttf/twin-ttf.c +++ b/tools/ttf/twin-ttf.c @@ -20,6 +20,10 @@ * PERFORMANCE OF THIS SOFTWARE. */ +#include +#include +#include + #include "twin-ttf.h" static double pos(FT_Pos x, outline_closure_t *c) @@ -35,9 +39,17 @@ static void command(char cmd, outline_closure_t *c) c->offset++; } +static int clamped; + +/* Coordinates are 2.6 fixed point in a signed byte: clamp, never wrap */ static void cpos(FT_Pos x, outline_closure_t *c) { - printf("0x%02x, ", ((int) (floor(64.0 * pos(x, c) + 0.5))) & 0xff); + int v = (int) floor(64.0 * pos(x, c) + 0.5); + if (v > 127 || v < -128) { + v = v > 127 ? 127 : -128; + clamped++; + } + printf("%d, ", v); c->offset++; } @@ -122,7 +134,7 @@ static int outline_cubicto(const FT_Vector *control1, void *user) { outline_closure_t *c = user; - command('2', c); + command('c', c); cpos(control1->x, c); cpos(control1->y, c); cpos(control2->x, c); @@ -150,39 +162,67 @@ static FT_ULong ucs_first_in_page(FT_ULong ucs4) return ucs4 & ~(UCS_PER_PAGE - 1); } -static void sanitize(char *in, char *out, int first) +#define MAX_UCS4 0x110000 +#define MAX_RANGES 64 + +static FT_ULong range_lo[MAX_RANGES], range_hi[MAX_RANGES]; +static int n_ranges; + +static int wanted(FT_ULong ucs4) { - char c; - - while ((c = *in++)) { - if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('c' == '_')) - ; - else if ('0' <= c && c <= '9') { - if (first) - *out++ = '_'; - } else - c = '_'; - *out++ = c; - first = 0; - } - *out++ = '\0'; + if (!n_ranges) + return ucs4 < MAX_UCS4; + for (int i = 0; i < n_ranges; i++) + if (range_lo[i] <= ucs4 && ucs4 <= range_hi[i]) + return 1; + return 0; } -static char *facename(FT_Face face) +/* Parse one code point; it must span the whole of [s, end) */ +static int parse_ucs4(const char *s, const char *end, FT_ULong *out) { - char *family = face->family_name; - char *style = face->style_name; - char *name = malloc(1 + strlen(family) + 1 + strlen(style) + 1); - - sanitize(family, name, 1); - strcat(name, "_"); - sanitize(style, name + strlen(name), 0); - return name; + char *stop; + + if (s == end || *s < '0' || *s > '9') + return 0; + *out = strtoul(s, &stop, 0); + return stop == end && *out < MAX_UCS4; } -#define MAX_UCS4 0x1000000 +/* "0x20-0x7e", "0xf095", or comma separated lists of either */ +static int parse_ranges(char *spec) +{ + for (char *tok = strtok(spec, ","); tok; tok = strtok(NULL, ",")) { + char *end = tok + strlen(tok); + char *dash = strchr(tok, '-'); + if (n_ranges == MAX_RANGES) + return 0; + if (!parse_ucs4(tok, dash ? dash : end, &range_lo[n_ranges])) + return 0; + if (!dash) + range_hi[n_ranges] = range_lo[n_ranges]; + else if (!parse_ucs4(dash + 1, end, &range_hi[n_ranges]) || + range_hi[n_ranges] < range_lo[n_ranges]) + return 0; + n_ranges++; + } + /* An empty list would silently mean the whole font */ + return n_ranges > 0; +} + +/* The symbol becomes part of a C identifier */ +static int valid_symbol(const char *s) +{ + if (!*s) + return 0; + for (; *s; s++) + if (!(*s == '_' || (*s >= '0' && *s <= '9') || + (*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z'))) + return 0; + return 1; +} -static int convert_font(char *in_name, int id) +static int convert_font(const char *in_name, const char *symbol) { FT_Library ftLibrary; FT_Face face; @@ -190,42 +230,52 @@ static int convert_font(char *in_name, int id) FT_ULong ucs4; FT_Int32 load_flags; outline_closure_t closure; - FT_ULong min_ucs4, max_ucs4; int *offsets; int ncharmap; if (FT_Init_FreeType(&ftLibrary)) return 0; - - if (FT_New_Face(ftLibrary, in_name, id, &face)) + if (FT_New_Face(ftLibrary, in_name, 0, &face)) return 0; - if (FT_Select_Charmap(face, ft_encoding_unicode)) return 0; load_flags = FT_LOAD_NO_SCALE | FT_LOAD_LINEAR_DESIGN; - closure.face = face; closure.offset = 0; - offsets = calloc(face->num_glyphs + 1, sizeof(int)); + if (!offsets) { + fprintf(stderr, "out of memory\n"); + return 0; + } - min_ucs4 = 0xffffff; - max_ucs4 = 0; - printf("/* Derived from %s */\n\n", in_name); + printf("/*\n * Generated by tools/ttf/twin-ttf from %s %s.\n", + face->family_name, face->style_name); + printf(" * Do not edit: regenerate from the font instead.\n */\n\n"); printf("#include \"twin_private.h\"\n\n"); printf("/* clang-format off */\n"); - printf("static const char outlines[] = {\n"); + printf("static const signed char outlines[] = {\n"); + /* Offset 0 is reserved: it marks code points the font lacks */ + printf(" 0,\n"); + closure.offset = 1; for (ucs4 = FT_Get_First_Char(face, &gindex); gindex != 0 && ucs4 < MAX_UCS4; ucs4 = FT_Get_Next_Char(face, ucs4, &gindex)) { + if (!wanted(ucs4) || offsets[gindex]) + continue; if (FT_Load_Glyph(face, gindex, load_flags) != 0) continue; - if (ucs4 < min_ucs4) - min_ucs4 = ucs4; - if (ucs4 > max_ucs4) - max_ucs4 = ucs4; - offsets[gindex] = closure.offset + 1; + /* twin_charmap_t stores 16-bit offsets: past that, glyphs would + * silently start in the middle of another one */ + if (closure.offset > UINT16_MAX) { + fprintf(stderr, + "error: outlines exceed %u bytes at U+%04lX; keep fewer " + "code points with -r\n", + UINT16_MAX, (unsigned long) ucs4); + free(offsets); + return 0; + } + offsets[gindex] = closure.offset; glyph(face->glyph->linearHoriAdvance, ucs4, &closure); FT_Outline_Decompose(&face->glyph->outline, &outline_funcs, &closure); command('e', &closure); @@ -233,6 +283,12 @@ static int convert_font(char *in_name, int id) } printf("};\n"); printf("/* clang-format on */\n\n"); + /* The runtime falls back to charmap[0], so it must exist */ + if (closure.offset == 1) { + fprintf(stderr, "error: no glyphs in the requested ranges\n"); + free(offsets); + return 0; + } printf("static const twin_charmap_t charmap[] = {\n"); ncharmap = 0; for (ucs4 = FT_Get_First_Char(face, &gindex); @@ -240,22 +296,29 @@ static int convert_font(char *in_name, int id) ucs4 = FT_Get_Next_Char(face, ucs4, &gindex)) { FT_ULong page = ucs_first_in_page(ucs4); FT_ULong off; - - printf(" { 0x%04x, {\n", ucs_page(page)); - for (off = 0; off < UCS_PER_PAGE; off++) { - FT_UInt g = FT_Get_Char_Index(face, page + off); - if ((off & 7) == 0) - printf("\t"); - printf("0x%04x, ", offsets[g]); - if ((off & 7) == 7) - printf("\n"); + int any = 0; + + for (off = 0; off < UCS_PER_PAGE; off++) + if (wanted(page + off) && + offsets[FT_Get_Char_Index(face, page + off)]) + any = 1; + if (any) { + printf(" {0x%04x, {\n", ucs_page(page)); + for (off = 0; off < UCS_PER_PAGE; off++) { + FT_UInt g = FT_Get_Char_Index(face, page + off); + int o = wanted(page + off) ? offsets[g] : 0; + if ((off & 7) == 0) + printf(" "); + printf("0x%04x,%s", o, (off & 7) == 7 ? "\n" : " "); + } + printf(" }},\n"); + ncharmap++; } - printf(" }},\n"); ucs4 = page + UCS_PER_PAGE - 1; - ncharmap++; } printf("};\n\n"); - printf("twin_font_t twin_%s = {\n", facename(face)); + printf("twin_font_t twin_font_%s = {\n", symbol); + printf(" .type = TWIN_FONT_TYPE_TTF,\n"); printf(" .name = \"%s\",\n", face->family_name); printf(" .style = \"%s\",\n", face->style_name); printf(" .n_charmap = %d,\n", ncharmap); @@ -263,22 +326,51 @@ static int convert_font(char *in_name, int id) printf(" .outlines = outlines,\n"); printf(" .ascender = "); cpos(face->ascender, &closure); - printf("\n"); - printf(" .descender = "); + printf("\n .descender = "); cpos(face->descender, &closure); - printf("\n"); - printf(" .height = "); + printf("\n .height = "); cpos(face->height, &closure); - printf("\n"); - printf("};\n"); + printf("\n};\n"); + if (clamped) + fprintf(stderr, "warning: clamped %d coordinates\n", clamped); + free(offsets); return 1; } +static void usage(const char *prog) +{ + fprintf(stderr, + "usage: %s [-n symbol] [-r ranges] font\n" + " -n symbol emit twin_font_ (default: \"font\")\n" + " -r ranges code points to keep, e.g. 0x20-0x7e,0xb0,0xf095\n", + prog); +} + int main(int argc, char **argv) { - if (argc < 2) + const char *symbol = "font"; + int i; + + for (i = 1; i < argc && argv[i][0] == '-'; i++) { + if (!strcmp(argv[i], "-n") && i + 1 < argc) { + symbol = argv[++i]; + if (!valid_symbol(symbol)) { + fprintf(stderr, "invalid symbol name: %s\n", symbol); + return 1; + } + } else if (!strcmp(argv[i], "-r") && i + 1 < argc) { + if (!parse_ranges(argv[++i])) { + usage(argv[0]); + return 1; + } + } else { + usage(argv[0]); + return 1; + } + } + if (i != argc - 1) { + usage(argv[0]); return 1; - - convert_font(argv[1], 0); - return 0; + } + return convert_font(argv[i], symbol) ? 0 : 1; }