From 6ac20a6eb0a0554efed7b321666465dd456cc74b Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 17:36:35 +0800 Subject: [PATCH 01/48] Add mobile and desktop WMs with a phone UI Window management becomes a policy chosen at run time: 'none' (kiosk, always built), 'desktop' (decorated overlapping windows) and 'mobile' (an application stack over the work area between system bars, with push/pop/dismiss transitions and an edge-swipe back gesture). The compositor stays policy-free; policies only place windows, pick focus and filter pointer gestures, and switching between them re-lays out every existing window. The desktop policy gains double-click maximize, edge snapping, corner resizing, focus cycling (Alt+Tab in SDL) and keeps title bars on screen. The mobile policy holds an edge press until the finger moves, so taps and long presses at the edge still reach the app, sends TwinEventBack before closing a window, and evicts the pixels of covered apps. Screen resizes, bar changes and an on-screen keyboard inset (twin_screen_set_inset) re-lay out the windows. A window destroyed from its own callbacks stays readable until the screen is idle. apps/cellphone ports the cellphone prototype onto Mado: a status and navigation bar, a lock screen, home and a dozen apps built on a small immediate-mode view kit. include/twin.h now holds only the public interface: object layouts and the backend interface live in src/twin_private.h, and applications use accessors. The compositor gains multi-rectangle damage, occlusion culling, a coverage cache, compositor-synthesized drop shadows and retained-pixel scrolling. Four headless suites (make check) run in-process with a virtual clock and bit-exact golden frames: core, wm, lifetime and the phone's whole user journey. They pass under every CI configuration, ASan/UBSan and Valgrind. docs/phone.md describes the design. --- .github/workflows/headless-test.yml | 50 + .github/workflows/main.yml | 73 + .gitignore | 5 +- Makefile | 96 +- README.md | 24 +- apps/animation.c | 36 +- apps/calc.c | 39 +- apps/cellphone/calc.c | 373 +++ apps/cellphone/calllog.c | 221 ++ apps/cellphone/camera.c | 523 +++++ apps/cellphone/contacts.c | 831 +++++++ apps/cellphone/data.c | 256 ++ apps/cellphone/data.h | 76 + apps/cellphone/dialer.c | 527 +++++ apps/cellphone/games-internal.h | 146 ++ apps/cellphone/games.c | 322 +++ apps/cellphone/home.c | 181 ++ apps/cellphone/lock.c | 168 ++ apps/cellphone/music.c | 883 +++++++ apps/cellphone/phone.h | 701 ++++++ apps/cellphone/photos.c | 741 ++++++ apps/cellphone/pong.c | 209 ++ apps/cellphone/settings.c | 1048 +++++++++ apps/cellphone/shell.c | 725 ++++++ apps/cellphone/sms.c | 658 ++++++ apps/cellphone/snake.c | 190 ++ apps/cellphone/tetris.c | 279 +++ apps/cellphone/ui-input.c | 257 ++ apps/cellphone/ui-internal.h | 28 + apps/cellphone/ui-paint.c | 649 ++++++ apps/cellphone/ui.c | 523 +++++ apps/clock.c | 12 +- apps/image.c | 27 +- apps/main.c | 133 +- apps/multi.c | 105 +- apps/spline.c | 28 +- assets/photos/ATTRIBUTION.md | 32 + assets/photos/thumbs/apollo16_jump_thumb.jpg | Bin 0 -> 1722 bytes .../thumbs/bob_kerrey_portrait_thumb.jpg | Bin 0 -> 1627 bytes assets/photos/thumbs/iss_window_thumb.jpg | Bin 0 -> 449 bytes assets/photos/thumbs/landscape_1677_thumb.jpg | Bin 0 -> 1733 bytes .../thumbs/solar_magnetic_loops_thumb.jpg | Bin 0 -> 2073 bytes .../photos/thumbs/us_constitution_thumb.jpg | Bin 0 -> 1299 bytes assets/photos/view/apollo16_jump_view.jpg | Bin 0 -> 16420 bytes .../photos/view/bob_kerrey_portrait_view.jpg | Bin 0 -> 9198 bytes assets/photos/view/iss_window_view.jpg | Bin 0 -> 1311 bytes assets/photos/view/landscape_1677_view.jpg | Bin 0 -> 11926 bytes .../photos/view/solar_magnetic_loops_view.jpg | Bin 0 -> 15619 bytes assets/photos/view/us_constitution_view.jpg | Bin 0 -> 14368 bytes backend/drm.c | 29 +- backend/fbdev.c | 27 +- backend/headless-shm.h | 23 +- backend/headless.c | 96 +- backend/linux_vt.h | 2 +- backend/sdl.c | 157 +- configs/Kconfig | 168 +- docs/backends.md | 10 +- docs/phone.md | 329 +++ include/twin.h | 1153 ++++----- mk/common.mk | 5 +- scripts/gen-shadow-lut.py | 64 - src/anim.c | 139 ++ src/animation.c | 38 +- src/api.c | 8 + src/box.c | 14 +- src/button.c | 5 + src/cache.c | 177 ++ src/closure.c | 45 +- src/draw-builtin.c | 23 +- src/draw-common.c | 505 ---- src/draw-pixman.c | 5 +- src/fixed.c | 28 - src/font-icons.c | 1764 ++++++++++++++ src/font-montserrat.c | 2060 +++++++++++++++++ src/font.c | 84 +- src/font_default.c | 2 +- src/icon.c | 2 +- src/image-gif.c | 6 +- src/image-png.c | 63 +- src/image-tvg.c | 4 +- src/label.c | 11 + src/matrix.c | 25 - src/memstats.c | 15 + src/path.c | 475 ++-- src/pixmap.c | 331 +-- src/screen-ops.c | 71 +- src/screen.c | 943 ++++++-- src/text.c | 213 ++ src/timeout.c | 54 +- src/toplevel.c | 34 +- src/trig.c | 53 - src/twin_private.h | 924 +++++++- src/widget.c | 107 +- src/window.c | 1120 ++++----- src/wm-desktop.c | 748 ++++++ src/wm-mobile.c | 628 +++++ src/wm.c | 306 +++ src/work.c | 17 +- tests/golden/cellphone.txt | 63 + tests/golden/core.txt | 4 + tests/harness.c | 476 ++++ tests/harness.h | 123 + tests/phone-contacts.c | 206 ++ tests/phone-dialer.c | 179 ++ tests/phone-games.c | 132 ++ tests/phone-music.c | 189 ++ tests/phone-photos.c | 167 ++ tests/phone-regress.c | 94 + tests/phone-scenes.c | 94 + tests/phone-settings.c | 294 +++ tests/phone-sms.c | 126 + tests/phone-test.h | 65 + tests/test-cellphone.c | 272 +++ tests/test-core.c | 997 ++++++++ tests/test-lifetime.c | 341 +++ tests/test-wm.c | 1306 +++++++++++ tools/headless-ctl.c | 108 +- tools/perf.c | 106 +- tools/ttf/Makefile | 2 +- tools/ttf/twin-ttf.c | 185 +- 120 files changed, 26654 insertions(+), 3160 deletions(-) create mode 100644 apps/cellphone/calc.c create mode 100644 apps/cellphone/calllog.c create mode 100644 apps/cellphone/camera.c create mode 100644 apps/cellphone/contacts.c create mode 100644 apps/cellphone/data.c create mode 100644 apps/cellphone/data.h create mode 100644 apps/cellphone/dialer.c create mode 100644 apps/cellphone/games-internal.h create mode 100644 apps/cellphone/games.c create mode 100644 apps/cellphone/home.c create mode 100644 apps/cellphone/lock.c create mode 100644 apps/cellphone/music.c create mode 100644 apps/cellphone/phone.h create mode 100644 apps/cellphone/photos.c create mode 100644 apps/cellphone/pong.c create mode 100644 apps/cellphone/settings.c create mode 100644 apps/cellphone/shell.c create mode 100644 apps/cellphone/sms.c create mode 100644 apps/cellphone/snake.c create mode 100644 apps/cellphone/tetris.c create mode 100644 apps/cellphone/ui-input.c create mode 100644 apps/cellphone/ui-internal.h create mode 100644 apps/cellphone/ui-paint.c create mode 100644 apps/cellphone/ui.c create mode 100644 assets/photos/ATTRIBUTION.md create mode 100644 assets/photos/thumbs/apollo16_jump_thumb.jpg create mode 100644 assets/photos/thumbs/bob_kerrey_portrait_thumb.jpg create mode 100644 assets/photos/thumbs/iss_window_thumb.jpg create mode 100644 assets/photos/thumbs/landscape_1677_thumb.jpg create mode 100644 assets/photos/thumbs/solar_magnetic_loops_thumb.jpg create mode 100644 assets/photos/thumbs/us_constitution_thumb.jpg create mode 100644 assets/photos/view/apollo16_jump_view.jpg create mode 100644 assets/photos/view/bob_kerrey_portrait_view.jpg create mode 100644 assets/photos/view/iss_window_view.jpg create mode 100644 assets/photos/view/landscape_1677_view.jpg create mode 100644 assets/photos/view/solar_magnetic_loops_view.jpg create mode 100644 assets/photos/view/us_constitution_view.jpg create mode 100644 docs/phone.md delete mode 100755 scripts/gen-shadow-lut.py create mode 100644 src/anim.c create mode 100644 src/cache.c delete mode 100644 src/draw-common.c create mode 100644 src/font-icons.c create mode 100644 src/font-montserrat.c create mode 100644 src/text.c create mode 100644 src/wm-desktop.c create mode 100644 src/wm-mobile.c create mode 100644 src/wm.c create mode 100644 tests/golden/cellphone.txt create mode 100644 tests/golden/core.txt create mode 100644 tests/harness.c create mode 100644 tests/harness.h create mode 100644 tests/phone-contacts.c create mode 100644 tests/phone-dialer.c create mode 100644 tests/phone-games.c create mode 100644 tests/phone-music.c create mode 100644 tests/phone-photos.c create mode 100644 tests/phone-regress.c create mode 100644 tests/phone-scenes.c create mode 100644 tests/phone-settings.c create mode 100644 tests/phone-sms.c create mode 100644 tests/phone-test.h create mode 100644 tests/test-cellphone.c create mode 100644 tests/test-core.c create mode 100644 tests/test-lifetime.c create mode 100644 tests/test-wm.c diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 6ec53cd2..f3e39ad1 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -33,9 +33,14 @@ 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 + export MADO_SCENE=desktop timeout 30s ./demo-headless 2>demo_stderr.log & DEMO_PID=$! echo "Started demo-headless with PID $DEMO_PID" @@ -77,6 +82,7 @@ jobs: - name: Test deterministic rendering run: | + export MADO_SCENE=desktop # Run twice and compare outputs for determinism echo "First run..." timeout 10s ./demo-headless & @@ -121,6 +127,7 @@ jobs: - name: Memory leak detection with Valgrind run: | + export MADO_SCENE=desktop DEBUGINFOD_URLS= # Run with Valgrind for leak detection timeout 15s valgrind \ --leak-check=full \ @@ -157,3 +164,46 @@ jobs: exit 1 fi echo "No definite memory leaks detected" + + - name: Phone end-to-end + run: | + # Boot the phone shell, unlock, open Calculator, type, screenshot + MADO_SCENE=mobile 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..cf74f307 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,8 @@ jobs: backend/** apps/** tools/** + tests/** + configs/** .clang-format Makefile - name: Set has_code_related_changes @@ -48,6 +50,77 @@ jobs: 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-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: | + make defconfig + make check \ + CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined" \ + LDFLAGS="-fsanitize=address,undefined" + + 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_DESKTOP=n WM_MOBILE=n + - name: mobile only + options: WM_DESKTOP=n + - name: desktop only + options: WM_MOBILE=n + - name: no coverage cache + options: CACHE_SIZE=0 + - name: no mobile eviction + options: WM_MOBILE_EVICT=n + - name: no allocator stats + options: MEMORY_STATS=n + - name: no widget toolkit + options: WIDGETS=n + - name: RGB565 phone surfaces + options: DEMO_CELLPHONE_RGB16=y + - name: TLSF heap + options: MEM_TLSF=y MEM_TLSF_POOL_SIZE=4194304 + - name: Pixman renderer + options: RENDERER_PIXMAN=y + 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..2947f0dd 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/wm.c \ src/dispatch.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 policies ('none' is part of wm.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 @@ -149,7 +158,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/cellphone # Emscripten size optimization libapps.a_cflags-$(CC_IS_EMCC) += -Oz @@ -204,7 +235,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 +247,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 +311,41 @@ headless-ctl_ldflags-y := # -lrt endif endif +# Headless tests: built only for 'make check' / 'make check-update' +TESTS := test-core test-wm test-lifetime +ifeq ($(CONFIG_DEMO_CELLPHONE), y) +TESTS += test-cellphone +endif + +ifneq ($(filter check check-update,$(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 +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 + @for t in $(TESTS); do MADO_UPDATE_GOLDEN=1 ./$$t; done + # Build system integration -CFLAGS += -include config.h +override CFLAGS += -include config.h # Ensure composite-decls.h exists before including build rules # (needed for dependency generation in mk/common.mk) @@ -290,9 +353,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..cbd65469 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,29 @@ Run the demo program: $ ./demo-sdl ``` -Once the window appears, you can move the windows and interact with the widgets. +The default scene is a phone: a lock screen (drag the knob to unlock), a +home screen and a dozen apps drawn with vector graphics on the mobile window +manager. Drag from the left edge of an app to go back. Other scenes run +the same window system under a different window-manager policy: + +```shell +$ MADO_SCENE=desktop ./demo-sdl # overlapping, decorated windows +$ MADO_SCENE=none ./demo-sdl # no window manager at all +``` + +Each policy can be compiled out (`WM_DESKTOP`, `WM_MOBILE`) and switched at +run time with `twin_screen_set_wm()`. 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 and compare every +incrementally updated frame against a full recomposite: + +```shell +$ make check # core, window manager 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..f1e75bf6 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -27,8 +27,8 @@ 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; + twin_animation_t *a = twin_pixmap_animation(anim->pix); + if (a) { current_frame = twin_animation_get_current_frame(a); twin_animation_advance_frame(a); } else { @@ -40,7 +40,8 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) .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); + 0, TWIN_SOURCE, twin_pixmap_width(current_frame), + twin_pixmap_height(current_frame)); } static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) @@ -50,9 +51,7 @@ static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) apps_animation_data_t *anim = (apps_animation_data_t *) twin_custom_widget_data(custom); 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(twin_pixmap_animation(anim->pix)); } static twin_dispatch_result_t _apps_animation_dispatch(twin_widget_t *widget, @@ -69,6 +68,16 @@ static twin_dispatch_result_t _apps_animation_dispatch(twin_widget_t *widget, 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; } @@ -88,8 +97,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); @@ -113,10 +122,13 @@ void apps_animation_start(twin_screen_t *screen, int y) { 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); + if (!pix) + return; + twin_toplevel_t *toplevel = twin_toplevel_create( + screen, TWIN_ARGB32, TwinWindowApplication, x, y, + twin_pixmap_width(pix), twin_pixmap_height(pix), name); + twin_custom_widget_t *anim = + apps_animation_create(twin_toplevel_box(toplevel), pix); (void) anim; twin_toplevel_show(toplevel); } diff --git a/apps/calc.c b/apps/calc.c index aa433cf6..f8be61e1 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -196,6 +196,17 @@ static twin_dispatch_result_t _apps_calc_button_clicked(twin_widget_t *widget, return TwinDispatchDone; } +/* The display is destroyed with the window: release the calculator then */ +static twin_dispatch_result_t _apps_calc_display_event(twin_widget_t *widget, + twin_event_t *event, + void *data) +{ + (void) widget; + if (event->kind == TwinEventDestroy) + free(data); + return TwinDispatchContinue; +} + void apps_calc_start(twin_screen_t *screen, const char *name, int x, @@ -207,13 +218,16 @@ void apps_calc_start(twin_screen_t *screen, calc->toplevel = twin_toplevel_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + twin_box_t *top = twin_toplevel_box(calc->toplevel); 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); + twin_widget_t *display = twin_label_widget(calc->display); + twin_widget_set(display, APPS_CALC_VALUE_BG); + twin_widget_set_callback(display, _apps_calc_display_event, calc); + twin_label_set_align(calc->display, TwinAlignRight); + twin_widget_set_shape(display, TwinShapeLozenge); + calc->keys = twin_box_create(top, TwinBoxHorz); for (int i = 0; i < APPS_CALC_COLS; i++) { calc->cols[i] = twin_box_create(calc->keys, TwinBoxVert); for (int j = 0; j < APPS_CALC_ROWS; j++) { @@ -221,14 +235,13 @@ void apps_calc_start(twin_screen_t *screen, 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; + twin_widget_t *key = twin_button_widget(calc->buttons[b]); + twin_widget_set(key, APPS_CALC_BUTTON_BG); + twin_widget_set_callback(key, _apps_calc_button_clicked, calc); + 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]])); } } diff --git a/apps/cellphone/calc.c b/apps/cellphone/calc.c new file mode 100644 index 00000000..4ea47235 --- /dev/null +++ b/apps/cellphone/calc.c @@ -0,0 +1,373 @@ +/* + * 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 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; +} + +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 (!c->operand || c->accumulator > INT64_MAX / SCALE || + c->accumulator < INT64_MIN / SCALE) + goto fail; + c->accumulator = c->accumulator * SCALE / c->operand; + 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->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); + if (len >= BUF_LEN - 2 || (!c->frac_div && len >= 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->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->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->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 = 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..cd82e90b --- /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, 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..67d52053 --- /dev/null +++ b/apps/cellphone/camera.c @@ -0,0 +1,523 @@ +/* + * 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 (e->dx < -40 || e->dx > 40) + 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..9fbec2da --- /dev/null +++ b/apps/cellphone/contacts.c @@ -0,0 +1,831 @@ +/* + * 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 PLUS_W 40 +#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) +{ + return ui_rect(PHONE_W - PLUS_W, 0, PLUS_W, 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, "Contacts", TWIN_SYM_PLUS); + if (l->plus_down) { + ui_rect_t p = plus_rect(); + ui_rrect(px, ui_inset(p, 4), 6, ui_alpha(th->primary, 0x40)); + ui_icon(px, TWIN_SYM_PLUS, FONT_HEADING, (p.left + p.right) / 2, + (p.top + p.bottom) / 2, th->primary_dk); + } + } +} + +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; + ui_view_every(&l->view, FRESH_MS, fresh_done); + 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, "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..38843e89 --- /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), 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..8febe75d --- /dev/null +++ b/apps/cellphone/home.c @@ -0,0 +1,181 @@ +/* + * 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 44 +#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; + 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); +} + +static ui_rect_t plate_rect(ui_rect_t cell) +{ + return ui_rect(cell.left + (CELL_W - PLATE) / 2, cell.top + 6, 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); + twin_argb32_t label = + 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 plate = plate_rect(cell); + phone_app_icon(px, i, plate, i == h->pressed); + ui_text_fit(px, FONT_SM, + ui_rect(cell.left, plate.bottom + 2, CELL_W, 16), UI_CENTER, + label, 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(plate_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)); + return true; +} + +/* Plate of @app; empty when the app's page is not shown */ +static ui_rect_t shown_plate(ui_view_t *v, int app) +{ + if (app / PER_PAGE != ((home_t *) v)->page) + return (ui_rect_t) {0, 0, 0, 0}; + return plate_rect(cell_rect(app)); +} + +static void set_pressed(home_t *h, int app) +{ + ui_view_set_pressed(&h->view, &h->pressed, app, shown_plate); +} + +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)); + return true; + case UI_DRAG: { + set_pressed(h, -1); + twin_coord_t pan = h->page * PHONE_W - 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) + ui_view_tween(v, 220, TwinEaseOutCubic, settle_step); + 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; +} diff --git a/apps/cellphone/lock.c b/apps/cellphone/lock.c new file mode 100644 index 00000000..ee50e682 --- /dev/null +++ b/apps/cellphone/lock.c @@ -0,0 +1,168 @@ +/* + * 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_cos((twin_angle_t) l->phase); + 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); + 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); + 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..0c3f6501 --- /dev/null +++ b/apps/cellphone/music.c @@ -0,0 +1,883 @@ +/* + * 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; +} + +static int32_t player_pos(void) +{ + return player.playing ? (int32_t) (twin_now() - player.start) : 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 ((int32_t) (twin_now() - player.start) >= track_ms(player.track)) { + 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 = 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, "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..e0478020 --- /dev/null +++ b/apps/cellphone/phone.h @@ -0,0 +1,701 @@ +/* + * 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 + */ +typedef struct { + const char *name; + twin_argb32_t primary, primary_dk; + twin_argb32_t statusbar, statusbar_grad; + twin_argb32_t navbar, navbar_grad; + twin_argb32_t bg, bg_grad; + twin_argb32_t card; + 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); +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. + */ + +/* Screen header: title, optional trailing text or symbol */ +void ui_header(twin_pixmap_t *px, + ui_rect_t r, + 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; +} 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); + +/* Ask the status/nav bars to repaint (after navigation or theme change) */ +void phone_bars_refresh(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); + +/* Window manager scene (Settings): TwinWmMobile, TwinWmDesktop, TwinWmNone */ +bool phone_set_scene(twin_wm_mode_t mode); + +/* 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); +} 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..ff06e892 --- /dev/null +++ b/apps/cellphone/photos.c @@ -0,0 +1,741 @@ +/* + * 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, TWIN_ARGB32); + 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_format(pix) != TWIN_ARGB32 || + 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; + +static void gallery_acquire(void) +{ + if (gallery.refs++) + return; + for (int i = 0; i < N_PHOTOS; i++) + gallery.thumb[i] = 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, "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..04641133 --- /dev/null +++ b/apps/cellphone/settings.c @@ -0,0 +1,1048 @@ +/* + * 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 BACK_W 40 +#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_WM, + PAGE_TIME, + PAGE_ABOUT, +} page_id_t; + +static const char *const page_titles[] = { + "Settings", "Display", "Sound", "Wallpaper", + "Window manager", "Date & time", "About", +}; + +typedef enum { + IT_SECTION, + IT_LINK, /* id: page */ + IT_RADIO, /* id: group, arg: value */ + 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; + +enum { GROUP_THEME, GROUP_WM }; + +/* Info values; those from VAL_HEAP on are refreshed once per second */ +enum { + VAL_NAME, + VAL_SCREEN, + VAL_WM, + 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; + twin_argb32_t accent; + 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) */ + twin_wm_mode_t pending_wm; + 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 const char *wm_name(twin_wm_mode_t mode) +{ + switch (mode) { + case TwinWmDesktop: + return "Desktop"; + case TwinWmNone: + return "None"; + case TwinWmMobile: + default: + return "Mobile"; + } +} + +static twin_wm_mode_t current_wm(void) +{ + return phone_screen() ? twin_screen_get_wm(phone_screen()) : TwinWmMobile; +} + +/* Without the phone's nav bar there is no Back: offer one in the header */ +static bool has_back(void) +{ + return current_wm() != TwinWmMobile; +} + +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_WM: + snprintf(buf, len, "%s", wm_name(current_wm())); + 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, + twin_argb32_t accent, + const char *label, + const char *sub) +{ + if (s->n >= MAX_ITEMS) + return; + s->items[s->n++] = (item_t) {kind, id, arg, sym, accent, label, sub}; +} + +static void section(page_t *s, const char *label) +{ + add(s, IT_SECTION, 0, 0, NULL, 0, label, NULL); +} + +static void info(page_t *s, const char *label, int id) +{ + add(s, IT_INFO, id, 0, NULL, 0, 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, UI_RGB(0x3b82f6), + "Display", NULL); + add(s, IT_LINK, PAGE_SOUND, 0, TWIN_SYM_VOLUME, UI_RGB(0xec4899), + "Sound", NULL); + add(s, IT_LINK, PAGE_WALLPAPER, 0, TWIN_SYM_IMAGE, UI_RGB(0xf97316), + "Wallpaper", NULL); + section(s, "System"); + add(s, IT_LINK, PAGE_WM, 0, TWIN_SYM_MOBILE, UI_RGB(0x8b5cf6), + "Window manager", NULL); + add(s, IT_LINK, PAGE_TIME, 0, TWIN_SYM_CLOCK, UI_RGB(0x10b981), + "Date & time", NULL); + add(s, IT_LINK, PAGE_ABOUT, 0, TWIN_SYM_LIST, UI_RGB(0x64748b), "About", + NULL); + break; + case PAGE_DISPLAY: + section(s, "Theme"); + for (int i = 0; i < phone_theme_count(); i++) + add(s, IT_RADIO, GROUP_THEME, i, NULL, 0, phone_theme_at(i)->name, + NULL); + section(s, "Screen"); + add(s, IT_SLIDER, PREF_BRIGHTNESS, 0, NULL, 0, "Brightness", NULL); + break; + case PAGE_SOUND: + section(s, "Audio"); + add(s, IT_SLIDER, PREF_VOLUME, 0, NULL, 0, "Volume", NULL); + section(s, "Feedback"); + add(s, IT_SWITCH, PREF_VIBRATE, 0, TWIN_SYM_BELL, UI_RGB(0xef4444), + "Vibrate", NULL); + add(s, IT_SWITCH, PREF_KEYPAD, 0, TWIN_SYM_KEYBOARD, UI_RGB(0x6366f1), + "Keypad tones", NULL); + break; + case PAGE_WALLPAPER: + add(s, IT_WALLPAPERS, 0, 0, NULL, 0, "Wallpapers", NULL); + break; + case PAGE_WM: + section(s, "Scene"); + add(s, IT_RADIO, GROUP_WM, TwinWmMobile, NULL, 0, "Mobile", + "App stack with phone bars"); +#if defined(CONFIG_WM_DESKTOP) + /* Only offer policies that were compiled in */ + add(s, IT_RADIO, GROUP_WM, TwinWmDesktop, NULL, 0, "Desktop", + "Overlapping, title bars"); +#endif + add(s, IT_RADIO, GROUP_WM, TwinWmNone, NULL, 0, "None", + "Every window full screen"); + add(s, IT_NOTE, 0, 0, NULL, 0, NULL, + "Desktop and None hide the status and nav bars. Come back here " + "to return to Mobile."); + break; + case PAGE_TIME: + add(s, IT_CLOCK, 0, 0, NULL, 0, "Clock", NULL); + section(s, "Format"); + add(s, IT_SWITCH, PREF_24H, 0, TWIN_SYM_CLOCK, UI_RGB(0x10b981), + "24-hour time", NULL); + section(s, "Region"); + add(s, IT_INFO, VAL_TZ, 0, TWIN_SYM_GLOBE, UI_RGB(0x3b82f6), + "Time zone", NULL); + break; + case PAGE_ABOUT: + section(s, "Device"); + info(s, "Name", VAL_NAME); + info(s, "Screen", VAL_SCREEN); + info(s, "Window manager", VAL_WM); + 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 + */ +static void paint_header(page_t *s, twin_pixmap_t *px) +{ + const phone_theme_t *th = phone_theme(); + twin_coord_t w = ui_view_width(&s->view); + ui_rect_t r = ui_rect(0, 0, w, HEADER_H); + twin_coord_t x = 6; + /* Same look as ui_header(), with room for a back arrow */ + ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 96)); + if (has_back()) { + ui_icon(px, TWIN_SYM_LEFT, FONT_NORMAL, 16, HEADER_H / 2, th->primary); + x = 28; + } + ui_text_in(px, FONT_HEADING, ui_rect(x, 0, w - x - 8, HEADER_H), UI_LEFT, + th->text, s->view.title); + ui_separator(px, r); +} + +static void paint_plate(twin_pixmap_t *px, + twin_coord_t cy, + const char *sym, + twin_argb32_t accent) +{ + ui_rect_t plate = ui_rect(10, cy - PLATE / 2, PLATE, PLATE); + ui_rrect_grad(px, plate, 7, accent, ui_darken(accent, 14)); + ui_icon(px, sym, FONT_SM, plate.left + PLATE / 2, cy, UI_RGB(0xffffff)); +} + +/* 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(10, 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) +{ + if (it->id == GROUP_THEME) + return phone_theme_index() == it->arg; + return (int) current_wm() == 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; + case PAGE_WM: + snprintf(buf, len, "%s", wm_name(current_wm())); + 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]; + + ui_fill(px, r, + i == s->pressed ? ui_mix(th->card, th->primary, 70) : th->card); + if (it->sym) { + paint_plate(px, cy, it->sym, it->accent); + x = 10 + PLATE + 10; + } else if (it->kind == IT_RADIO && it->id == GROUP_THEME) { + paint_swatch(px, cy, it->arg); + x = 10 + PLATE + 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) + paint_header(s, px); +} + +/* + * 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); + } +} + +/* Switching scenes re-lays out every window, this one included: do it + * from a timer once the current event has been fully dispatched. */ +static twin_time_t apply_scene(ui_view_t *v) +{ + page_t *s = (page_t *) v; + phone_set_scene(s->pending_wm); + refresh_others(s); + ui_view_invalidate_all(v); + return -1; +} + +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; + if (it->id == GROUP_THEME) { + phone_theme_set(it->arg); /* repaints every window */ + } else { + s->pending_wm = (twin_wm_mode_t) it->arg; + ui_view_every(&s->view, 0, apply_scene); + } + 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 (e->y < HEADER_H && e->x < BACK_W && has_back()) { + ui_view_close(v); + return true; + } + 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..3ce41a20 --- /dev/null +++ b/apps/cellphone/shell.c @@ -0,0 +1,725 @@ +/* + * 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 "phone.h" + +/* + * Themes + */ +static const phone_theme_t themes[] = { + { + .name = "Olive", + .primary = UI_RGB(0x7a9b5e), + .primary_dk = UI_RGB(0x5d7a45), + .statusbar = UI_RGB(0xc5cdb5), + .statusbar_grad = UI_RGB(0xa8b09c), + .navbar = UI_RGB(0xa8b09c), + .navbar_grad = UI_RGB(0xc5cdb5), + .bg = UI_RGB(0xcfd7ba), + .bg_grad = UI_RGB(0xc5cdb0), + .card = UI_RGB(0xeff1e8), + .text = UI_RGB(0x222222), + .text_sec = UI_RGB(0x5d6253), + .separator = UI_RGB(0xc9cdbf), + .call_green = UI_RGB(0x7a9b5e), + .call_red = UI_RGB(0xbd4b4b), + .missed_red = UI_RGB(0xb84040), + .sms_sent = UI_RGB(0xdce8c0), + .sms_recv = UI_RGB(0xeff1e8), + .indicator = UI_RGB(0xa8b09c), + .on_primary = UI_RGB(0xffffff), + }, + { + .name = "Dark", + .primary = UI_RGB(0x42a2ff), + .primary_dk = UI_RGB(0x2080e0), + .statusbar = UI_RGB(0x000000), + .statusbar_grad = UI_RGB(0x000000), + .navbar = UI_RGB(0x1a1a1a), + .navbar_grad = UI_RGB(0x1a1a1a), + .bg = UI_RGB(0x121212), + .bg_grad = UI_RGB(0x121212), + .card = UI_RGB(0x2c2c2c), + .text = UI_RGB(0xe0e0e0), + .text_sec = UI_RGB(0x9e9e9e), + .separator = UI_RGB(0x3a3a3a), + .call_green = UI_RGB(0x66d06d), + .call_red = UI_RGB(0xef534d), + .missed_red = UI_RGB(0xd3392f), + .sms_sent = UI_RGB(0x2a5e40), + .sms_recv = UI_RGB(0x383838), + .indicator = UI_RGB(0x555555), + .on_primary = UI_RGB(0xffffff), + }, + { + .name = "Ocean", + .primary = UI_RGB(0x1e88c8), + .primary_dk = UI_RGB(0x166a9c), + .statusbar = UI_RGB(0xdcecf6), + .statusbar_grad = UI_RGB(0xbcd6e8), + .navbar = UI_RGB(0xbcd6e8), + .navbar_grad = UI_RGB(0xdcecf6), + .bg = UI_RGB(0xe6f1f8), + .bg_grad = UI_RGB(0xd2e4f0), + .card = UI_RGB(0xffffff), + .text = UI_RGB(0x1c2833), + .text_sec = UI_RGB(0x5b6b78), + .separator = UI_RGB(0xd5dee5), + .call_green = UI_RGB(0x2eae5f), + .call_red = UI_RGB(0xd64545), + .missed_red = UI_RGB(0xc53030), + .sms_sent = UI_RGB(0xc8e4f8), + .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); + twin_window_queue_paint(w); +} + +void phone_bars_refresh(void) +{ + _repaint_window(g_status); + _repaint_window(g_nav); +} + +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) { + ui_vgrad(px, r, th->bg, th->bg_grad); + 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(0x22c55e), app_dialer_open}, + {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x3b82f6), app_contacts_open}, + {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x10b981), app_sms_open}, + {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xf59e0b), app_calc_open}, + {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xec4899), app_music_open}, + {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xf97316), app_photos_open}, + {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6b7280), app_camera_open}, + {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x64748b), app_settings_open}, + {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x8b5cf6), app_calllog_open}, + {"Snake", NULL, glyph_snake, UI_RGB(0x14b8a6), app_snake_open}, + {"Pong", NULL, glyph_pong, UI_RGB(0x6366f1), app_pong_open}, + {"Tetris", NULL, glyph_tetris, UI_RGB(0xef4444), app_tetris_open}, +}; + +#define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) + +int phone_app_count(void) +{ + return N_APPS; +} + +const phone_app_t *phone_app_at(int idx) +{ + return (idx >= 0 && idx < N_APPS) ? &apps[idx] : NULL; +} + +int phone_app_find(const char *name) +{ + for (int i = 0; i < N_APPS; i++) + if (!strcmp(apps[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); + 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->text); + ui_icon(px, TWIN_SYM_WIFI, FONT_SM, 34, PHONE_STATUS_H / 2, th->text); + char t[8]; + phone_format_time(t, sizeof(t)); + ui_text_in(px, FONT_SM, r, UI_CENTER, th->text, t); + ui_icon(px, TWIN_SYM_BATTERY, FONT_NORMAL, PHONE_W - 16, PHONE_STATUS_H / 2, + th->text); + ui_fill(px, ui_rect(0, PHONE_STATUS_H - 1, PHONE_W, 1), + ui_alpha(th->text, 0x20)); +} + +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); + twin_window_queue_paint(w); + } + 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), ui_alpha(th->text, 0x20)); + 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); + if (i == active || i == g_nav_pressed) + ui_rrect(px, r, 6, + ui_mix(th->navbar, th->primary, + i == g_nav_pressed ? 150 : 100)); + 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, th->text); + ui_text(px, FONT_SM, cx, r.bottom - 5, UI_CENTER, th->text, 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; +} + +/* Destroy every app view except home and @keep, without animation */ +static void _drop_views(ui_view_t *keep) +{ + for (twin_window_t *w = twin_screen_first_window(g_screen); w;) { + twin_window_t *next = twin_window_next(w); + ui_view_t *v = ui_view_of(w); + if (v && v != g_home && v != keep && + twin_window_style(w) == TwinWindowApplication) + 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_views(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 bool is_app_view(twin_window_t *w) +{ + ui_view_t *v = ui_view_of(w); + return v && twin_window_is_mapped(w) && + twin_window_style(w) == TwinWindowApplication; +} + +ui_view_t *phone_top(void) +{ + ui_view_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_view(w)) + top = ui_view_of(w); + return top; +} + +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_view(w) && ui_view_of(w) != g_home) + n++; + return n; +} + +void phone_open(int app) +{ + const phone_app_t *a = phone_app_at(app); + if (!a || !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) +{ + ui_view_t *top = phone_top(); + if (top && top != g_home) + ui_view_close(top); +} + +void phone_home(void) +{ + ui_view_t *top = phone_top(); + if (!top || top == g_home) + return; + /* Hidden screens go first (no animation), then the visible one pops */ + _drop_views(top); + ui_view_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); +} + +bool phone_set_scene(twin_wm_mode_t mode) +{ + if (!g_screen || !twin_screen_set_wm(g_screen, mode)) + return false; + phone_bars_refresh(); + return true; +} + +/* + * 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; + if (twin_screen_get_wm(screen) != TwinWmMobile) + twin_screen_set_wm(screen, TwinWmMobile); + + 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_home) { + 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 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)) + 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..50dc79cd --- /dev/null +++ b/apps/cellphone/sms.c @@ -0,0 +1,658 @@ +/* + * 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_header(px, r, "", NULL); + ui_avatar(px, 20, PHONE_HEADER_H / 2, 10, name[0], ui_avatar_color(name)); + ui_text_fit(px, FONT_HEADING, ui_rect(32, 0, PHONE_W - 40, PHONE_HEADER_H), + UI_LEFT, th->text, 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), "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..12515571 --- /dev/null +++ b/apps/cellphone/ui-internal.h @@ -0,0 +1,28 @@ +/* + * 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; +} + +#endif /* _UI_INTERNAL_H_ */ diff --git a/apps/cellphone/ui-paint.c b/apps/cellphone/ui-paint.c new file mode 100644 index 00000000..519b9e82 --- /dev/null +++ b/apps/cellphone/ui-paint.c @@ -0,0 +1,649 @@ +/* + * 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; + 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; + +bool ui_init(void) +{ + if (!shape_path) + shape_path = twin_path_create(); + if (!user_path) + user_path = twin_path_create(); + if (shape_path && user_path) + 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); + shape_path = user_path = 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 + */ +void ui_header(twin_pixmap_t *px, + ui_rect_t r, + const char *title, + const char *trailing) +{ + const phone_theme_t *th = phone_theme(); + ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 96)); + ui_text_in(px, FONT_HEADING, + ui_rect(r.left + 6, r.top, ui_w(r) - 40, ui_h(r)), UI_LEFT, + th->text, title); + if (trailing) { + ui_rect_t t = ui_rect(r.right - 40, r.top, 40, ui_h(r)); + if (_is_symbol(trailing)) + ui_icon(px, trailing, FONT_HEADING, (t.left + t.right) / 2, + (t.top + t.bottom) / 2, th->primary); + else + ui_text_in(px, FONT_SM, + ui_rect(r.left, r.top, ui_w(r) - 4, ui_h(r)), UI_RIGHT, + th->primary, trailing); + } + ui_separator(px, r); +} + +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, ui_mix(th->bg, th->text_sec, 40)); + ui_text_in(px, FONT_SM, ui_rect(r.left + 6, r.top, ui_w(r) - 6, 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) + ui_rrect(px, r, radius, 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); +} + +static const twin_argb32_t avatar_palette[] = { + 0xff3b82f6, 0xff10b981, 0xfff59e0b, 0xffec4899, + 0xff8b5cf6, 0xff14b8a6, 0xffef4444, 0xff6366f1, +}; + +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 + 8; + 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..e8766f4e --- /dev/null +++ b/apps/cellphone/ui.c @@ -0,0 +1,523 @@ +/* + * 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); + twin_window_queue_paint(w); +} + +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 (a scene switch) */ + 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); + 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); + for (int i = 0; i < UI_VIEW_TIMERS; i++) { + if (v->tweens[i].handle) + twin_tween_cancel(v->tweens[i].handle); + v->tweens[i].handle = NULL; + } + 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 */ + 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; + } +} + +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; + if (s->flinging) { + s->flinging = false; + return true; /* a press that stops a fling is not a tap */ + } + return false; + 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; + ui_view_every(view, 16, _fling_step); + } + 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..b4fc0718 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -217,6 +217,15 @@ static twin_dispatch_result_t _apps_clock_dispatch(twin_widget_t *widget, 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; } @@ -252,7 +261,8 @@ void apps_clock_start(twin_screen_t *screen, { 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); + twin_custom_widget_t *clock = + apps_clock_create(twin_toplevel_box(toplevel)); (void) clock; twin_toplevel_show(toplevel); } diff --git a/apps/image.c b/apps/image.c index 262fd8f5..71585c04 100644 --- a/apps/image.c +++ b/apps/image.c @@ -62,8 +62,8 @@ 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_SOURCE, twin_pixmap_width(img->current_pix), + twin_pixmap_height(img->current_pix)); } static twin_dispatch_result_t _apps_image_dispatch(twin_widget_t *widget, @@ -122,11 +122,12 @@ 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, 1, 1, _apps_image_dispatch, + sizeof(apps_image_data_t)); if (!custom) return NULL; @@ -138,17 +139,18 @@ 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; + 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) @@ -156,7 +158,8 @@ void apps_image_start(twin_screen_t *screen, const char *name, int x, int y) 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); + twin_custom_widget_t *img = apps_image_create( + twin_toplevel_box(toplevel), twin_screen_height(screen) * 3 / 4); (void) img; twin_toplevel_show(toplevel); } diff --git a/apps/main.c b/apps/main.c index 29175988..3d0428f2 100644 --- a/apps/main.c +++ b/apps/main.c @@ -20,12 +20,16 @@ #include "apps_multi.h" #include "apps_spline.h" +#if defined(CONFIG_DEMO_CELLPHONE) +#include "phone.h" +#endif + #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, @@ -42,37 +46,45 @@ static twin_pixmap_t *load_background(twin_screen_t *screen, const char *path) 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 twin_wm_mode_t scene; + static void cleanup(void) { +#if defined(CONFIG_DEMO_CELLPHONE) + if (scene == TwinWmMobile) + phone_shutdown(); +#endif twin_destroy(tx); tx = NULL; } @@ -84,13 +96,34 @@ static void sigint_handler(int sig) exit(1); } -/* Initialize demo applications based on build configuration. - * Shared by both native and WebAssembly builds. +/* + * Scene: which window manager the demo starts under. Kconfig picks the + * default; MADO_SCENE=mobile|desktop|none overrides it at run time. */ -static void init_demo_apps(twin_context_t *ctx) + +static twin_wm_mode_t pick_scene(void) { - twin_screen_t *screen = ctx->screen; -#if defined(CONFIG_WINDOW_MANAGER) + const char *env = getenv("MADO_SCENE"); + if (env) { + if (!strcmp(env, "mobile")) + return TwinWmMobile; + if (!strcmp(env, "desktop")) + return TwinWmDesktop; + if (!strcmp(env, "none")) + return TwinWmNone; + } +#if defined(CONFIG_DEMO_SCENE_MOBILE) + return TwinWmMobile; +#elif defined(CONFIG_DEMO_SCENE_DESKTOP) + return TwinWmDesktop; +#else + return TwinWmNone; +#endif +} + +static void init_desktop_apps(twin_screen_t *screen) +{ + (void) screen; #if defined(CONFIG_DEMO_MULTI) apps_multi_start(screen, "Demo", 100, 100, 400, 400); #endif @@ -109,12 +142,14 @@ static void init_demo_apps(twin_context_t *ctx) #if defined(CONFIG_DEMO_IMAGE) apps_image_start(screen, "Viewer", 20, 20); #endif -#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. - */ +} + +/* Without a window manager every window covers the screen, so overlapping + * demos would hide each other: start only the first one. + */ +static void init_single_app(twin_screen_t *screen) +{ + (void) screen; #if defined(CONFIG_DEMO_CLOCK) apps_clock_start(screen, "Clock", 10, 10, 200, 200); #elif defined(CONFIG_DEMO_CALCULATOR) @@ -126,13 +161,52 @@ static void init_demo_apps(twin_context_t *ctx) #elif defined(CONFIG_DEMO_IMAGE) apps_image_start(screen, "Viewer", 20, 20); #endif +} + +/* Initialize demo applications based on build configuration. + * Shared by both native and WebAssembly builds. + */ +static void init_demo_apps(twin_context_t *ctx) +{ + twin_screen_t *screen = twin_context_screen(ctx); + +#if defined(CONFIG_DEMO_CELLPHONE) + if (scene == TwinWmMobile) { + if (phone_boot(screen)) + return; + fprintf(stderr, + "cellphone: out of memory, showing the classic demos\n"); + } #endif - twin_screen_set_active(screen, screen->top); + if (!twin_screen_set_wm(screen, scene)) + scene = twin_screen_get_wm(screen); + if (scene == TwinWmDesktop) { +#if defined(CONFIG_WM_DESKTOP) + twin_screen_set_background( + screen, load_background(screen, ASSET_PATH "/tux.png")); +#endif + init_desktop_apps(screen); + } else { + init_single_app(screen); + } } int main(void) { - tx = twin_create(WIDTH, HEIGHT); + int width = WIDTH, height = HEIGHT; + + scene = pick_scene(); +#if defined(CONFIG_DEMO_CELLPHONE) + if (scene == TwinWmMobile) { + width = PHONE_W; + height = PHONE_H; + } +#else + if (scene == TwinWmMobile) + scene = TwinWmDesktop; +#endif + + tx = twin_create(width, height); if (!tx) return 1; @@ -148,12 +222,7 @@ int main(void) 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")); + twin_screen_set_cursor(twin_context_screen(tx), cursor, hx, hy); #endif /* Start application with unified API (handles native and WebAssembly) */ diff --git a/apps/multi.c b/apps/multi.c index 5bebb93c..32a4ca04 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -6,6 +6,7 @@ */ #include +#include #include "apps_multi.h" #define D(x) twin_double_to_fixed(x) @@ -147,8 +148,8 @@ static void apps_multiline_start(twin_screen_t *screen, screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, "Lines"); twin_custom_widget_t *custom = twin_custom_widget_create( - &toplevel->box, 0xffffffff, 0, 0, 1, 1, _multiline_dispatch, - sizeof(multiline_data_t)); + twin_toplevel_box(toplevel), 0xffffffff, 0, 0, 1, 1, + _multiline_dispatch, sizeof(multiline_data_t)); if (custom) { multiline_data_t *data = @@ -179,9 +180,10 @@ static void apps_circletext_start(twin_screen_t *screen, { 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_rect_t client = twin_window_client(window); + int wid = client.right - client.left; + int hei = client.bottom - client.top; + twin_pixmap_t *pixmap = twin_window_pixmap(window); 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); @@ -220,7 +222,7 @@ static void apps_smiley_start(twin_screen_t *screen, int x, int y, int w, int h) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_pixmap_t *pixmap = window->pixmap; + twin_pixmap_t *pixmap = twin_window_pixmap(window); twin_window_set_name(window, "Smiley"); @@ -271,9 +273,10 @@ static void apps_ascii_start(twin_screen_t *screen, int x, int y, int w, int h) { 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_rect_t client = twin_window_client(window); + int wid = client.right - client.left; + int hei = client.bottom - client.top; + twin_pixmap_t *pixmap = twin_window_pixmap(window); 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); @@ -346,7 +349,7 @@ static void apps_flower_start(twin_screen_t *screen, int x, int y, int w, int h) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_pixmap_t *pixmap = window->pixmap; + twin_pixmap_t *pixmap = twin_window_pixmap(window); twin_path_t *stroke = twin_path_create(); twin_path_t *path = twin_path_create(); twin_path_translate(path, D(200), D(200)); @@ -367,6 +370,54 @@ static void apps_flower_start(twin_screen_t *screen, int x, int y, int w, int h) twin_window_show(window); } +/* + * 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]; + for (int i = 0; i < n; i++) { + uint32_t sum[4] = {0, 0, 0, 0}, count = 0; + for (int k = i - r; k <= i + r; k++) { + if (k < 0 || k >= n) + continue; + for (int c = 0; c < 4; c++) + sum[c] += (tmp[k] >> (c * 8)) & 0xff; + count++; + } + twin_argb32_t v = 0; + for (int c = 0; c < 4; c++) + v |= (twin_argb32_t) (sum[c] / count) << (c * 8); + p[i * step] = v; + } +} + +static void box_blur(twin_pixmap_t *px, int r) +{ + 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_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); +} + static void apps_blur(twin_screen_t *screen, int x, int y, int w, int h) { twin_pixmap_t *raw_background = NULL; @@ -378,36 +429,34 @@ static void apps_blur(twin_screen_t *screen, int x, int y, int w, int h) 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_rect_t client = twin_window_client(window); + int client_width = client.right - client.left; + int client_height = client.bottom - 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_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(client_width)), + twin_fixed_div(twin_int_to_fixed(twin_pixmap_height(raw_background)), + twin_int_to_fixed(client_height))); + 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); + client_width, client_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); + box_blur(scaled_background, 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); + twin_composite(twin_window_pixmap(window), 0, 0, &srcop, 0, 0, NULL, 0, 0, + TWIN_SOURCE, client_width, client_height); twin_pixmap_destroy(scaled_background); twin_pixmap_destroy(raw_background); diff --git a/apps/spline.c b/apps/spline.c index 4d339990..3b8c09e3 100644 --- a/apps/spline.c +++ b/apps/spline.c @@ -201,17 +201,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, 1, 1, _apps_spline_dispatch, + sizeof(apps_spline_data_t)); if (!custom) return NULL; @@ -231,18 +237,19 @@ 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, @@ -254,7 +261,8 @@ void apps_spline_start(twin_screen_t *screen, { 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); + twin_custom_widget_t *spline = apps_spline_create( + twin_toplevel_box(toplevel), 4, twin_screen_height(screen) * 2 / 3); (void) spline; twin_toplevel_show(toplevel); } 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 0000000000000000000000000000000000000000..3b75065588b5cd1a11c4164bf386a06107187be8 GIT binary patch literal 1722 zcmb7;d05g38pnU|Lr}&GQS$>e_pB4Gi|^>gyYsTJAM6HaF4N z-)Fne+zNxU#u@IlbFjlYSYmP5FA@j>fk3Jv(MTj3You?4{lD4i2T-a&AP@*b4ggRT z1Vll0h5-`*fP#=+fd2qf0YEqcq6*zj-LwHH1cbt1pbG4(e;+`gAOJ(D?AJl#z2FDJ z6Lky95qjpC1Ns(@hrE4~C>3|wg(B(dZg;JT0+Pmf5zzhWeKmfWc`Y#YDYCj(I z(m`kLya3ce$nJk23LpaO*-`A^l?SnpRw6P=+Q;~m(-~lyqo>DUr})Mxnr>OZ%Jb5V zwiDUqh8*Qoa)A}K>w<*p{j`CAy&Kx*hb?J|VtZNdt}L(;`%BiU{Khh4 z$zjpPZM@Vqek;S7o0X@vNZ`vCy3z+CtL#I=896xhn>kg@ zT^+H5RVG2^dXXbYoP;ZS^!m3_ndqk*ORDu@ylT6&?MwY(voLKxPG@o3V{^V% z%lppsT&pZ7YF8C)|7_sYhIzUhJV$R-1}X2A3?#$&}-cZbhy~{UEH*S_S(n1{7}c z9;-WP+@f~6DriU*K5Dvo=m!BVtUS))58b>-hyc;UcNx9F6woa<${-qOVsiyI$tfGJpF4ra1Fd@p~eBZ)< zY^~+>!rvdhdgjbfBGjf`z0D;=(|Pu1o+f*4-pAr2qU{_lx{#=` zSpH1>RiVYe6CG$sPVXNGk&{w(zEHUKH2S!=cNy)6`p(Ivx3Nr2^MQ`;sW&3>d7A_U zo`)kxjmXZa+FfI2lW!g#xiRGSdYr0qpIb>eZWa6)Xv|)YZ2Tj4Z%N3hiN}U}$2Eq3 z@#s-->^Pi~`kaSFC@;%=gkHDUptF-9?hH~(AiSSVQJ|ZpF*>vBqe)bTQD%zbw6Up& zsl7C{@QKrNg=X_RnvG5@B==H-UFU`k$4jcxu&<2WFRWG=F0Z|=X$`_14WDDPEE;| z^M@0eiPt?RBF`M1c&Mz|U$sOE7^halHYx(7>gdpinn=l_fgEK}cloTf+-K->BT>rT zZ0Y+o-G_LMqHgYzG=l_)fhNoOIv!>(-}H@GMq#doUr~~6MWo^nE}mMlV5hORwuaL^ zU33gZ=JmE;Fn82u!aa~aLbCt%oSNa0_?DB(*3kQ{Uc>W+esf2$k_Uy3Ah)LOI(+DZ6U%tIyK)^fMu@!jY1xzf%r9b% zVEl1(*?ZIRA}?-Zp^vc1wl|B*b%1goc1BXy$!uidX1i=H=!mA$|_`}XI z#gmgSswZyqsn#|J{N|R}jD?au--;>=a)RY{*<=ba=v8{0TUg8KXu68qN}~G z^qV^t1#&GKB^4HpZ=_PwWN$|ClzxG7HgzRpMSD`cD5@JLO>#Ciqc^zMYV&J&7*|a8 z8xG!Sdu$}?TgnWbb*H%zH-s*75&V9iR#(~{Q+WaQa-WOGZ>fW}_wJRGM@S#k6)gu# zL$a6xJB$O=MIqfRy6kjIqZ-8nz|85o4e~wfwdqv`1IU!n+CAspH6O7HXfXqp!{Rkc n%qif=U58Pz6%#d28Rvbj+f7HpSM=8bJuU$?9BZOfNJJ* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5549704b1036852f07b2ed0f877bac6dc794e89a GIT binary patch literal 1627 zcmb7;dpOez9L9gM*=!Y?%i88Pv#~9TTw950Me8C&j5^XGZLe}3NY`@ZTy^(z3$+_ZTU00M!4KQ#xa zhk*?M6rv5)hCrd(P#6rVgFqq>a5%zHS8pkDxgiF#+z^e%nUc(L#+D{%G~NbpX=P2J zQn2Q>cD7_Y5}88&6#|06ULL>r_926Qyk1qpz_V30Nx0)c}6b!Z3#&_-E6^$qNOVFY?ue3_x&^?oDt z>MWv_=iU=t4%y|JM+^w~PbdKViiBv01quLbDmA6Q8#M@JVXsez5PZA()kK}1GdZci zK>jq|-a??&@1s%!5%LOQ@f>Zn7o70*#@3wr{G!OU^4Iqmjl^gjOYRgsZhd~HY$m)) z;o4Ngq`zck_~3Q*s9{!bzC7RJq>{{TNu`{_>(awLqHE?uS6KaRM{LaNPRet-xwe{0 zif!y?1lUBZ2;T1UM3khe;wfYX&FSMr04aUe z2EEjGz)e7WlN&hmhq3Vv`OATBZWH5(l<$>xK|+TZM$m8{+>#3pId1iK$1c^XZHugH zxB4WF8i+cZ!^v#kjWdbP(-oQ#3f(S+Dhl35vyi81W2gAms&Q-jBqciO;NBy1cEO$uhEwJApq zbs4|FWDzjNEMJ?tfc3n+FBU)8y!zZt*!MH_jupMzc9lFbjTynIS z_;d_Qaq5z-F=}AGIEa|rsn?zeZ3rY_sIQD~{Yo-Q5`-~5n3 z8R>+3_tJ6#=1NaXoA=?uSj@b7^!+Pon@_%mq;rduT=`c=#iw?Wcrar4g&N4(R$W>g z`h36SyHvf=Rqx6d+AL(dFL{{0%M_t$6zd(4 zs|#rcE9S3%ZcNCnmIQ4F!P?f$$|+p})SH}b+_7h_c{Npr_<4D~r;BNlS=gq0W0mdW zR#)Mpm4pJ! z>`uL*KBc(nE*@_(5x;S)Z2UX&TKhZ2ae7~r2M>Dn3S+Xh?#vJW^&3xxtffS-$2>FT k(!R28V&e0K9nBLR{%XLV;gh47sh)G&22nW?htxy=04w^c%>V!Z literal 0 HcmV?d00001 diff --git a/assets/photos/thumbs/iss_window_thumb.jpg b/assets/photos/thumbs/iss_window_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ecfc6593510052c66845d59359f685fff22074b2 GIT binary patch literal 449 zcmex=^lOiET&UP@Y7ModgWM?qOlT~kX_QeM|USHnP6LsJ7}2qQZ?I~NC+Fc+7w zhLo6;2Fc+60R}-11}_FLW=16jCP7AKLB{__7$g}O8JK~NMFPw~XL10gR0J3q7+IJY znOWhIj7-cdf~;(Y?2ds#!Xk-{Kvkk3uc88wxgvt-q746UG4L=m0gyPSFD?Re}72D{jTJ%42#aZx^qv? zt?_qp%vOh240TeF7G+4}R2+kwgN b-{sxxxDp({=SYngmxBnZBQbdF|8D{S8@F$I literal 0 HcmV?d00001 diff --git a/assets/photos/thumbs/landscape_1677_thumb.jpg b/assets/photos/thumbs/landscape_1677_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..184a05efed803a49b1d7fa9288066d0712a245d7 GIT binary patch literal 1733 zcmb7;X;_j80>@uKK&eqdML-P?O!3OALO`u)CAIM^4>BuF^JuIw&2oj*nyefFK`c|$ zFc_~1l@#)r4l7cd$;xXL@rW`r@71X-jdAD0KF@sI-`D@C`Ilw_(D$eL(E!i}g`h$J z&?tdD089%CgKEKGP#7Ez(?;kc5IQ;tLtVYi`o@MBjIkjajWe^x<4mkf(P(oAbE~a3 zL^~oD@96Ak>uhaHwEb@h2o8rMv=Igfgn=y%jkEo~(L4Z<+JHac4*^*MU?d2F1Zf@v zrT_qfZkz@F2NVp^g2A;xz=lfI2f!c*1b{(x;LyJuATR{bLPAj%Fawv}A*qH&mfV+c zE7#DN+@hw3Xo6ck(@{FR1oNajcY4F3Bt2#mCVpbT6>c3Z}1 zMu5!_(8fy$67UALxJSF4{lcuC+}0`Dnk(ii%9B3XFIHVm4ocX1t%)qd`awD+qYUb5 z`-{ZsuQ$f4>FPE6MKPQGJ`(@_+PS3}<4M}>*#!QM0R^Qj|4_Ykokcs>f+dakg^`h= zR*2p~CuUs1)wGrBlzS3si1ON{U28QKt<&jeQG28h$K0BW&RhB%v9+NoaD0rhTz3#rr0kxO#?SYsc z!=?44T`ePCW z@Lbs0>(3w+y*T;m-Wd)PI(FU66CjFf9V^~>?5dCA~+twD9{~xr{eMKgI z1+!|Bdbu`yL^ON9!*8g&{d!uFGAt^ycaiO?o76cs64yCDyH2#OK1UXMw(fY&S}CPa zN_cPoIJi?fHfZFx@FbC(A&#%1h>IVmxdMTG3hIXncl+tXJyY<|SAGw=7J6An+KeP4 ziF3sC2&X#INgu*~_BVC>3Uy$A%E75o1%Wuu?edCkV&9-+x07Boe>b6hAvZM_ae0g< zN%ei@&aB&o=f~_=eX7&r!9xx*%%JwfadT%6>zw&pvWU2%1K}$HgeT16tNBE{?;Aa8 z_`8>LUYnxHqmNEDdOc?_R|P9kAuZC@%pdFW56Q*`tf&bJ$>6Vc?C-vWG|qQfHuFnj z2j1L)dW!N7!&%{?_e{QrD<47eO};X2TS_9t9%H+rL~3t@V}+n*=b5s`G}Xn>uxCFE z+dRMGBrtO`Z9*;n|Ajr`*?4XQkHRa zD(CQtfHMrj@!*>&rbj`g$Cnez@NPJgcTnQT;GbD-ohDeDo`TqVVVpGT_V!kkXCYtp zdUUFMb#4Bh>e2P@52Zd3a{-n68IYT9F25DUCL-8ILbXF6_GO zrk_?2+p*G>)nKYeZtz@)o_%WQEMQtkS9_)RhGxBme?3s=bxBOlP8s6=CI2R z9E8hW#!z4CW9w=3tJL={_Sr}9<+P%}mTx%zE%=e)8ZK{lw~ZP923#J1mD%?-2=yYA qTcE3wEK+E;|Gx)>4483xZ7GxlXLmciIUy0*!Zbu5!DE!5LOghvchl2MjqH^(+Il5I#A zow1Y@WxX0SMOh=^$_zbOnx=3)_nzkuxbM&ToX-!h^M0Mr{)hc}K+4U@)d>KB0Kny7 zfcj#)d3iaEtgNDn7FJOSrz|V0Mo`0P z>*(s~Dqsza4e-WVcwPKo5)d2?M~EWO2m~6hD65G7zq8*BphN*TzzqV@0Kg~^1O?jf z1(W~)0{Sb!-#Pdd0tbnL51^S800u!IUh@O0=jHW8~tPulj;zV+{lAQuA8OuqIK4jqDcto+HN3?}ty^s;DwD)GCjiZO`vh zv-i3EQDG{qIM5(k_i2VbVP5taPidrVD`qG}YncAM0ACW)T`yz3dExBHKH&PX_k0(d z_BN^RYpA99_+FDC|D?J0+^mz|;bvHgfccB8a>9Rk+Vg3ERn^&)YZsCl!ruv7-Wj=i zl4_E%jo9Vt^+<)$Cr=jgU3QN$#A{+VkixzEK&jN!7h$*UGPEXEa9Yc$^1{5N@z^)4 zJU~F*`ezbX10UO;w>bA%Jnh@j4}I405&X@~8Wof50qizTXm7V3Oz2Kwd?9itxM$hp zr~Ep6JE-HhyCmM(1p1om)?Nko0eK_9XK>G(_lmGQ@5Pv9j(q!MF=G3t15(&` z{kIT99d>g8(TymVi93_mv}7rE^C;gYkenk#EQDu z>%&aT`G$hus1okm*&|8Wo(=uYCi+J@l`&9*YmvJf|lW&z+ z}V}TOB+3L_p(Qp3k(+*20Lz@#z5ok~-P z{1-6_LGS{yT3w=YHqddmxrQ6t`UB5P3uRp`n)>6B5}&`T7KRn^2CILL;;fIbIz8_f z&6$L?I4eh2bW-eHR=P|)e&?}>VP|rzV3GQzRYMcUpf0l>_X{r{E_$QWT$=v4V5XVj zkTRNV%WT!DH|jJiD9a3uQ%)T1pSeaQTaZVay(l>A>{sX?Uc-VK^a-!%DD}`uoJx7N z?_x9f^qD1f(;*DlA+g>g<|;4n^Z6f35uCmH?820G*}ZMEg@Cre7KZ5MNWWTFU#B{y zjQ*Q`%*YR-JElf7@}6Ejh*PWR15%sbc^cwCv9|Ql3^=aP@)9=RRB0PjIKdO9c{ok$ zheq6kv;}I;JwE?URYYD8-&Dp9RjKZ+cFqweV(r%aga`$_r#*IMc`gVDy z@g;B9A%J@0;h~sv|F$tmgxP8*o|0&h^SzcleHrj;f35Yrg5$sWc}mzD278tU7B* zN}_A>NpSHaJ;paJi3ZxOn2xdH_1SjSxmeSEpu57d5_6PSt(lh%<(NZwCUhD#Sa0Ct c1)G;ETiiAsmC@^C=$tJw+8)dL^kRSVKYAFB6aWAK literal 0 HcmV?d00001 diff --git a/assets/photos/thumbs/us_constitution_thumb.jpg b/assets/photos/thumbs/us_constitution_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b2f753bf46246330424df47f948e3cb5c07e2ec GIT binary patch literal 1299 zcmex=^lOiET&UP@Y7ModgWM?qOlT~kX_QeM|USHnP6LsJ7}2qQZ?I~NC+Fc+7w zhLo6;2Fc+60R}-11}_FLW=16jCP7AKLB{__7^D~&n3zGXg##uQMrKwv2B4gg00R>v zGZPC7GY2z71tXIn1GA!#A&anMppsGIMB{}USw)ncf(jcCIu|{>2-G9N2nKM|7#J9t z7?@c>HVHzckn92)CuFGj{}uxe(0nFAWSo1zy^PX`0tWNUEx6x()&OCd0 z<=R8XDeP?7_AMJ_bek?ndmS^#j&Bd=UFpi!SqjWY?`Lxvs zSbY{vHmEUC4)KZ%{u3giy3D}xc;NXe)rWIbOn=&boV-_bw|ScH)|k4P%Z_d_t7(t( zX<6NP{_oO*bG3M1+D%R82&{-!@)O{nYI$r$z`_G8xer8Vr_A$8`047uF6>dxmdGs) z2CV8i@?IA=%yLZ2;XQX{KQDXBwl%8)R!A*Ye(t{_jMZC@LrdL0s+hyk;IPV;G&il( z)LOUWReRqqOtHDbl9F;ECWKGo4pZh@5p4z^9=@;7np^FzB;A^(K4aq*$CcF%JKLpm z)plw(M8304>YduA?4Og+H9JG(M?QmE)t{9e?LRIl`J653Skq^8v5ZY(<}IZu(X0+K zhkL^AUh1r8<4NX~k2<@X?e)_=$CpaYH7I-A8Xa!xcu{hB&g?3i)P85t`H#4BO^-1) z&+PZgbhdkC@=NBg*75_Tu9H-XdA_W&STLKt^sCxhUscD8IWO-xF4}Qx?R?hFFNIwV z);2V~43L=j%hhrzLINi5gchHC!K9cF^7eAXiEpdx z-seu7c9X42dN%W)kZlVK9k`9k9UY4vb5054G8N@iTgUBv=i!#ShaR!?rN{0LGyVM} zeutuq;j#5m*TVGXceUKr;C#BgMc(sl@f_E8%Ja($AK9{3nY!eDSRt7sa9~5=L$&!2 z*NN_2n!*>C6M3n8yh>3&@_=AGP06->0LLo%@Hwd5x0Fcm-{+sUq zYe*=lXz0iom@iam0st}+G8z&(3K|9~@(UvX2^j^I5P(KR&ntuennBCToj5EBgORWB z4~Z8V z_`~YyUWe+%Hi(CFr1dVq_eq<)Uh8QI)XR1+l!U?CG^ryPcDw$*zGz{bNm-EH;FL(N zI!&NV!d%amSQI(y5ADmP&0jNG4tVg-MM}>5>DaAO!2CtL`pGUaw?TG%Nc8&JQ^Ic zc>2L1JtKzImaGbMK;|wXXS(4^7g%Qb1MoGcrTe<+nk=liai{NSg7BAj_H0Lb*-fek@D3BN^E_brJ^tm zcYM#gTj=77P3m>N)FiMXg|Dv0-_H>eJz-fpWXRbEWy2woz8(@(6D_eVO zg|Aq?j-_ogB}eW5`op`AwC{hbYV|P#87^koCnAC$I`|lo$5y?b#+t)MM#j|i;IDfzxk6*~(3G!wrPx$V%{4 z9$}2#D2iN^B6%Y=Me6yF(dl}mNEvCf5}~Lj`GK114N4S@1~$%#J#sl8&w|a@>_3!M z?Y2X@JC50FKbYYPvT8Ra@g>f}j8wlyn{V~GuDR{Ux&|cb*n075fK0FWC%8Q(_#D*2 zfH7})?{efIrmc;BU>XHqoUA0n$-m(YDU~;JGC=rmraI`;>RX2ZmG66*c&w#acBhqZ zx}s$&PWuwoA6?|f)^F9lIfvB2ugB7-X}s!HlAK*%wXcwfN@Rk0YhnZ-$@aK;ktjemg#J6(Yty^ z@jRM~O3F&lv~4yrZT51VSeIkY)D^s4brlk7zuP*i&ke0RF-mU9DoSHK(q*bAMxHMQm>qi;W4KwWoNh&m!ru>)bB=A7Ja*H$(Pqc=JvqR4>U=yUw-XXgl`_ zUy`47+VQgOLl%mpnuJbkG~KjqZ=_Klqor>vF8VVOuY2l#CaQoKu;K()HCd{BBkGGU zr+b0BH8tVcL4rp1$c%&=XK>8T9mk>D&8?rno7=h^NkDb7Uz6!Es*UsVKS1P3{J}rK zd9)eL@s*$~-LlcZAW7iR&1#5*{|U#Po|jjGpn;=x*}~A^zzu-O1??J`V5+xAVwPKd z^1ewe`@<7Vi%yE|JapeVzJ)u^PYLT%7D?(CyLNU@=SS#ks)V=?twKZbGKmY<3Br(ZkK4}Wjxl}ml$b^; zTp9WH);!7mWAWpLI+pUH2bmKZnQ<1DmEOXr#WUwsyrv3-!sFFJ%Pq6O+P8mzk-*}{ zBi$!RtbMb33G(+jTvqV%Bg#zlgI9}0Rr5c9Jq4Fp)|g!Chu4*vz|-Bz(2Shm4Fi=! zjF2}iYwibYT_-bOrsHm-@G!nZ<%3Dd`=r-2XEk7Oq<9{eB=1(!TFway45X!EhN7Gv z;5&!~xFKakkRP>kIhRRT#I=ph&#K7laS9BmLV5Mcx+4dKR>&l46FO%qr4DQs%I|ez zzN0>WDr*r_-nSQR!R00w>c@Kd_pMo4a4cJJb$<4{aJ?qrtcxH+kfW_RFrrO-+ z_yb0@c4Me$+tB7yqmw?4nq_^oM`trc%E*>WpL12 zu@w+V$MaiDVh#H@)bYeea7PzJF;>U`^;jrexMa{*%r{~}T>PJyC~EQ=s+Hyj8~7Xg zkUpvq$yiI**Hc-LP`Em380jqI_6n88@E1U$$OTvy4dJV440{^~W}&Np+{Lyp4f1*k zcLkRgtTJ^1CBa@PEZwM%sz7>?3Wi>lH#hIf*uQl9(;{7SoO8?$ewKLWGpHh(1CdTP1=F1O%Xu z0&~;-(kj)~_c=5zS633&l6DfuHkNT~H&)?a8-`zLoJCQ5wYS034X@Qp_Cd;pV=zBl zD8OUqXV-V$55^k{ItMAhd*&8@RSIKimZWRvW6^$d4Lm*RbnRhx-trQc%=*Uk zoZ*G~0HRlP=^Li^Hj_@IrP0_%hect@mHa?4UG(OYo)uT#vIp+TgQ62OgFzjMUn=r! znAU9^i`w13WGK6VA9mAs^VRdDqztxu-HIMqm!jEd$2I5njGm}NJ~pUP3!>#Xt&hmJ zLhrKk)Hyv=7h*VzlNzEctjba+!_du@&9B?U;ieyj23?YzFI=Z~b&br`zT3@>5p~sM zJni)BdKRr7%vmb!6IG#qP)iR~uvuQlRw|w)qIB}uA?iaOY$cOgIgelS>o~OYe|%T6 zJ+d&56*$S{ti#c9VdK%w&wfX41Q7pS$rRxq=9eCnG~`XPB?&+}Ix znjkLD+Ni!PH`m^cCWuC8xQxr%;Y0bv<-O`IJts%g6+HpZ`rdXOt;v0?AHMIh13&KM zHU$h6r7UxPDk;fuEqHbOO5;?)sb^J#U&n2HOgR-x4;Rr6A-ZRzwMZEk* z^SM{g!|(n9IzmkSXHGbOtQr0ToO}@+mzf*Akh(oeI`3Xg*Yv??B2GDf;6i93J{&Qlh}8j9-_T z=jW5RmMM$5RDv$CvvT=A#R@E7D4zBK|>UYaQ zCNOPVIqp!+-FYqX7mrIU5)5pwr!`*``3zU>P|lmzvU>#tQB5a7y{JiPg4@j3oAU8q zEVi{FVkl`V9|d{RU-qasc1cP3diF$0g}+%U*@&s8ObmFxB~;L%DzR7=;Q7S z1&%aRqcXvUPAZdVBLE}<2++VuU}AfoC!FNu`~L$_s}$8fdm&rDOXk4tt$*cjKABBWhW{Calgg=OLE-x#LHD)&Y5m78lN0JbJahWJp zr~3HL_ue|CWx83B#c(oIMUHqBXz%>`BihWHZxha27;$yVGmasMosp< zo8e#~4Y2yt1Xc7|)9P&R+fPj8$!x7{shPE-EuBh}Y7=gjV)-s|Zv_g+%P$GF^iPPM z{_-hL;)2`SuA86Eer|oED~)gDDJ#q+pWt}9dOsaPS>7gd>Xz^x4~4=yksakWwO_cc zIpL>GpeJ;0Z2u6^Wa!i*6+44z+ApQN^$wF=(sBzx>R~;wcb(qH;7W8V z$sR%>bdEU>QM>oP8cuCFS1wc#^JL67H$SU#V*L=M_MPSyg@#iV8dJqWqxfzDfAp8o z${Wr<;Y`4Kwb7j%$0-KZiE&?c@QGV++h0V(^v_+GsJWw@i8x1tghyrMgei(m{p#N~ z8s6P=H4WUNJWw&H1hujeFfDNs^bgR(#YYYKV6=v=pYmN$2V&gUrm4QS{+X!Q`8xBh z18dn>Wt8st2alh9&-Cn6*(9zw_-LnS9pUc#S8gPns!6dO)oQPwiK;8{{{gy#M(S!M z#ZSbW=QT2($4hv!$cuGty%#<3YF(A?OlY+YuDe5yp%%1;d2jura17zW1ZXG0+#}^H z**dN8it_ai%Oz7R_=oW0n&@Dii9CrPq{h-8xh@@p;7@eLErdYZ4@OS;W zXf?#eA!nGNDkecyHlvC@MwTFi2!3`s9O3FeN@6G>k44$+>Xom-AyY2L9|~L~-64^n zB63x%GwXsvD8$mwT>BHqJf!z0b6W?Us*}EsYYQwk$0`g4SnF9e;`U@Dru1=-ama#( zt}T_^Y%cd@y3Jjt2P&$JM|_c6OOyyR3Bl!o`Hu@EC*!82i857An%U-{z3nX1slV}|?L>;h zvl&jH5f22B(MY!n_m)8Q zzU$BHUXK?ESK4UcjBtC*CcWlL@)i<;lHCZ6r1w%^T>2edoQd~HmBdJFg~v1*nXzH+ zegDip`1c^l6+6^LdS{hzvE(c|u$3`}emvkkm9B?`rl$OQA=<<^MFvWD4aJW4;C7yB z;~0&AH?za46K_{`J%lEVLaC^#G%G%ya=u)eIlfq~wtOea6}@|m@!VqH#WRm8 zpY$JK>dx+>GHSu8tGJWdsG`K#w_Lpdaz6J1vo(`EKgS1KCy`0ZC#@qnzr)GjOJ7e9 zVZ`(u;nN*Pg=;>RxhtT98EscNt7M1HZ~e_4wY<|q{bs{$_=+OIsp?X%V#4F%J4Jx7 zuYm<;s*&)Ys=S)nAGc?sDLJ(v;{Jaj$MLT1%vLgH=@Rw&Kb9*|YSI+(l?mM3p{>a4 zZE&BM`XyKAMBTkR8oo(qfevB63L8?Gi2-A|R6!gx%7{h#FY(pi$~2gel<++4fJVx2 z&BundBB60sL)+N21v?w~E2f(6nSKk}NZK90=00wWFr5f0=kFP@M>Liea0!pXwi?EqN^u8oIdpv@jT4dp}4nXUT`Ri2jO`7}iebGOUu?AmH%EcWbR6U;WVC zTpqGC7iH^u{io_H^8pnBae!aI*5~Hj!sM3jqqo>Vp`S_N?=5ivV(~qnWBr3Zl;ur` zT~E#Ko-RTD^54)8Lg>ZAdLwp%+lNxIeP%d(wGyk#b-y6%!Oi!EcM?HI`;mS*P`PD} zDP5DU;6F&sr-q12@_8Sjly{ESsf61TeL$>U9OQQ&37fBlw<)&|`GffVhJ-@$waT*_ zy$PmXV?^lY;-~B=Cea$U9|z9l{2Z-?z@)uY1x`il-)St|uFI=3_yMA&i6)h(OlJQ9 z-p!Ct&cC~zc2ir91|P6RmQK=Rcy@m&!0)nHL{JT;+VZjHoM85>9BgA5$vUta7d6O7 z0BPU)kiJoMa6EF@MTHzN9N=xFXtI5hxVjz)I@Qx4e8}kQzR_xz$j*}rhF3K28?`7o zJCO;Lm6uk;bSiB{*`6QHdw`vjwrEYMw&$c~8vLWVX6^Lmlx)+xvDQkyX7&RP@8Ws0 zMXzPAF2xssT829EYq}0tjc?YhcKj}AGrG#A?WY2`bM{1;!0$$OVdwmShhhq}QD~cx z0(3jjGljF}F|{4xaPc_OLP7!N14>U?;nA#_B0NAgo&jeEQMyklCD|fn@lF+;Q4FcM z1zPJusW7sD%gc?E1i|AtJ7%3_;+n~wh*UNu@jHZXcEnX`YOP-dY*43r@}E z)Khx_-F{28rIWcdQWS$&I|cJ}A))o}aFbY}2#=A=-Wf{xfXwPoMV``6G&FH3HjYB= zb3NBbyLVgfjl5Z{_4D`Sp-JgYLA_P!okho^aYKb4pkKRI6U!e~!qQLj}&f00DavDrI5TgH>U zebA6i_y@rD_Xa%V3J)|<7`SYDs}8)2%Pxqr(P!Kz>_HiASq z^2$fSuhNqhH^K<=Y4k0~N2pc9c{{^y@?{@14ke0JZzo2x5Kn1g_L1XONOLMCaS11} zW-bv^a^npE&n(iq#U&_CxSd+asi-IGy`L9MT>tg%-Un|}E8xUcz9x_&-=Se4SFf^4 zZ~%KQ)hHS{I*{Q8v3Rr}?`aHzDA^S?5Ftxg^H%%=9Lt}AFapl*Y`9I57UlG3CpdU3 z){JuHJg2h}l*#)oKEY&5sI!vqN8|*ul%iCfYj@x@@BIun^X3=(7aUB;ZGn2YXBn_0KBWOd(+^(CtVFbl?`I6jr5ZT!1z3Y zOvsm~2ZpblAGx?8G*9mNUuyz)@-j0UTEjCNQA@R`aSK1+rUwg2{1s^&@6^h&TQb!p zSePhiX^w?HK%pkD*mJ*?vO9UK{~A-G&x?mhf1wd`0rhH)A@CP}sxV0iOr_huX|l$y zwu$BISRQY+7#J~kuZxF@4fw75>167r*Oi~;W(|GMW!TpnF$eWL1#w2-cP%KDl$S;O zB))de^SK!M;e#P!7ghSX!$3SD#XMB_9Bun3Zb(W-P)(dujm!8Va)Bg?l^hvxG5-jT znL#`FTvsyXsk-azvD#C{qgz&%EL|@U$&H;- zUB~+mfY3N1bjUw2NLLm=bAXVyR;~BBAJ|Vwt${>(ip0ufPs52vf00*~pI5gR#3LOm z%u?sSpy(z~E~hQ5ex9KXHVjmvjKnO$}9-;C&b&OcUWb&cq7K&Ul? zERim^L!7q`#7RKc0ZT5^r5wY?(UtW{6r{WbEbZ)zxgCFpD=C-Ti*saDn;Ou6#!u74 zujbPqL^O^oSFbegbA+3p8l;#=R8nM6nR+1(B8VEE;*ZVM@^+KFjP&kzGtJV9Wtc{R$r-Hn<_eC_OD1t55B&b( zA@YsxIMVmYfKL{$E{EXL++n`&PHE`8T5flTGMb{xJi8;FWgoX^BK$NP6oF7<#tB*! z)2}OqNBy+iDmDf(dW^(zkLp6sr$J{C8BnLDFOe`3YZOELbUL?@qeW^(!`bA3cKHM? z=O@2`Bza(^3snf?$RXTEjMKOo+_Q5T&PXu;Cx_Lqa<woN%AW$0xl*;Jk=5c_=bs(zp4#{bC>r4p%ze zE5m!U`SZo{;~f^L!o4vnv0gwTt_hR$VGKzVD+O)wt7$UI+tW+^X(@c@q$NMMii)|Y z9{C5rpC}h^Pb2?jGhfz?9qj|{%{O=ZmX^;Q&*R8kz^L{}h_u^0qTbdp=`n7){Hd~0 z8Qb0={YNUka`{Vu4>`EUur^<`aKiMLj$IXuZ3rNbF}?97!Wf%Nvn0ElJ#%jOO2um^7Al=8l+(N;%Vm}w5ZAG?`*P6iDrFk734ZNG zZhq_ti@j)?7c%1;W71=i2LVqDQY{K40&JouFW4t=b{lme?iPlh2k1zbKoVQ}?qah$ z8d{uzUfq|NsipELT0j$_s4$f)ZEYItrdjmS_9zPys*Fe_;qP;!-pdU?ci5&Rrr!<%+} zpDv^u$In6px$SK4lf`R$YZ+p}zRX5F?f4-q%Im!<6JSNClF_E(G?el`i4H_WTu5t%O{1aJn46Fd8`JNQ6aCd zi$-p?XHeUKAfM_5R^dFk0xDBGHGy0E{RU7+Lr_MpBvsR>IjMGgu9mhai!Il64wYq) zuVPlFxz(bS?xs->#)!&0oW6n3+S+#!<>1OaQs^PP`7E=pRxNWWCHrg`4Rzhq$F`;6 zEQ~yA9wE08D2s};+&dI)BU^FNe+*0>9~Q&%R_q77x;%?F+?ywa+cmb_G}P``z*c5R zh^*)J6L5fdEGjn|Ze&+IPn<36Lz3jxpR)3}ZgUgyVCx$=I3`x#F2&SmClrbOf|y9S z5~ya37u}s#9gzo;y6c);`F086hs}{& zI5V;Gr=AV-@9ImWN*!{n$0+ z+55WBd9%KAzwam7}Zlj;e9Ij89C5kuxhW+vGdN?nyh@piZ z{}JKpN25W0s2>iT<4Qic4SiErQ=hnCkK4ftv;vl_CXa%|{mz`NHq&(Yfn_FX{{R_C z5jW&5Z?Kup70JP)Z=Hh!P9jRmOOu80XVj(tM8cij3ZB4c5tQZO{2J5|luqqNYN3Xy zDxQl4?N9FUrNuRA?_+mWbSpO>O;Pr@R$PPIdWzFXqF6U!P_`H{>**0#?im!ZLM_ph z2;x*JR$*VFiJ}v1Tlw7ILhe*|&H8o7<>#Zosf_(s6sj1L0HVOQ%MmvcC;pq1%(rWy zbFLbk3lDE*h#dLvO0qW@KA*h?+3NR%f-N=j-)YoLYAGG+MP`h0P*9Ew@9o`M)xFCK zTuy8hD2&v%=D_`y+j}T_Il{A-(hyOe_${@G4WpA4Bj%N5$h0K+F0t8)@2^iT!a-wy zQ6}!UP@#5byaKSKTXHnZS2HWPM!HRc-kNh{I>6|HHTmYzRixeBgJ~{DXL9-l!dw~g zKX~<9b24&-KWl=T*3Zi7sP8s2k08-`MGyyeV!L_q?6j4zyWmglk@% z;!ql%mV5%Kk*ljAuU>l!+@D|k8DFVX$JMRh?8hpUJ;>Rt(N7#M6q$T0GgiREB3*?( zN+UwKc>QRd`7`Qhbg;-Bf2OS_N*JJMba!kX#_E5#wnOOJRAIJ|h zvdv9MValcOtll#$t|y2<0(KvZHN>^kdGMAiD*$d{*1s>A0cM6{K%&Xb!E!{E4S2m=@Ca*2MB5LFn-dNt)`Yo@bK}`_3O&MyKWZ?bo91b4Y zJ&9+z5c5|u7s6oyzYhmktMBbEjz0MeUZt{DtczK)mShU}mVY+veM&%ixQq!GjklqQ zGqdsnbh3R#nKX?TMWBfd>0mjfCpNVD^tDrcUVMC&#^%rvb@3U5DtIOOg|N!3GMXUH z1FflC2uj_W#uCDEPN zq71^jlEwjWdz8qJ$|VHd7}!dEvl?+ z975wsM6SFL{e#%;MRAopi*M-QBpd_k{?e{WpcaT zg;IEdxELDr27&ZL);__KQGTyqKZfp?GT0~T+&G_e9FsKHpJ7v(gs)+={uzQaj_buq zMhVq3FCc>YM825@Y*tn*h`RusX$<}W$Sq^e1sb%Eq7E6^!nFC>!8_a@lHKRIWkr*w zqqdV(+ls(CpTnUbOiQ zF+0kfWKG_GfF!M%efMW~WYs@Y1`J8H`f$2V7s-zEE0HMVrWBAsEy(*PR=qrF_enrX zP~Gn!o>L>0b+tkYX+E>vcKco}D@*i8L@m0=39a7)S5l#hplWMN?h?2S6(4fqqVMA` z@gT`RVL+nChu-VQMvNEKLW;co@(FxekV$<%uQloTrJ*Xh?(LQ}_m(p_2OSw}nYl+n0}uEsbF)?(XST{v4@-nwFjW2tDYPAMlGQODm0};?eq` z!uQdDROv{L0kINzCI?ffC9#{c+d->NL?uP%`?KuSp~?a1V|Qp5qrI$JT8 zY$*OgjK$S{WYdn$2_9=oGI@BV;;35-ZMH}>5R6P$)zZ+3H`Mpo6~TVUkM$uJXL9YQ zKHeM3)`NpN3L~;3k8$iGrn;KANU7DWo|1+;;6uBqZ_*{P{l4eJ!b&YdLR&t&KH6jQ zgY`WfX6Y!_pVed;anE?;T1WA+?>xyH0!rQv_L3S}Q!~_CmDjme4$D%1_=*L0LI``y zWM-)TUi_GY$?KJChxX&ggmWlttYllGp)#|`?*u@F-&XWWm6hCEE#zu849}ul;-A>c z_4MI~jN}mvRaC_3IKp@u2?omUj!xnkQw9*q_up_Jt}CvRyjma~JTVHI?+54J!Ycuf z_g1mJylJIc+LxnE7j&m%s6?&`Dx01K5@0Nu^#yG?x?%&toQ9J?V=m$>D`HJfr$Q? zlb_&=AJa+7r=~$0mQ?a#S|P{WyDbFnOzv>oSx^2y)Q_;zq zp;l4RQ6W_1Z-*MNdnHW>QGlfME?R1pRa^&1|r_ajaM;%$~0wN!Mz$L z)LNuP=!zB*Rpp?7#bjyDA~lm#>qR7@MLrFJynQ z!)sfuY~9S>FK-R}mgF6_LDTTVZK}bCxM8Mkk{iaSBW&uQH<;wK&6zDHIeQs4&gTlC z8=KM5EKc^gzZ)7%Nj||N-r9_94W7Puj`FAsl-5q9OxZU*6cThNo>#r9o*LS-$t3b) zLg!wWk5CLxSs>dlKxJ?AiW3b6wTY`ck>j^uUB$A2pc6~9$Lkh&29u2D_?s?m0V5>l zjVs0k7f%Di(@~QZO>Gc>1#W?~>lGy_BPhtG@-D8Jj6ra#FW;tjF4oz@pMp6mMmTgB z30nkc`gr#vwvZkRSVWJaxCYHIJsT+-NASliQ@)CjAy*od9YlqroIIWbP0R+684hDp zISK>K%@H$;L=0uK!wY2MrYm(i7bA}NDcu?Of1g4)BtAs*7DktY!>on(XQ(Wj{`O;O za=4o0jy;F0;0XNEQLt%OaS@1HB#5`c^`POdsGF`=@khDXCK2=t7dX~N5ht`PFg46} zLL^AYy=L?D5t#md{9|v1AtrTwvHNEE79!Y8Liy$NSFs^GfiUoy?%mfvb5Vtm=qxf` z7T|B(s_F|QpjXerHbGWlo)L)vPC)}1hoLJ-_+WSiZt>bU$@q1S`hFP#vs%_vXVc{idFo#);~b`8~&q6 z7_+Fda?{f)Ve;W5G2zT64Tee50z6y~yD2eH_ng&kzLWiG`3*UYu%jAR-=i{#E$%|X`3Ku z<7+6ud;iudD>8;B(OiK`3)D{53p8~RJDUS6S4~e+3mC%*)E6{;R|=Y^ZyQPqH2?&0 zRL;;919AT+UXrcT@a722;b3v`ROUJjbm1P>rwK2fUgp z)YKD;4&vDv*qWzmU>EA^@Epp~%1wo&AjX6`Q=q(eu$hK_K-nXQR|T#M)|2~DIGb)1 zp^bhJCZSQrZIq-m{CxU3BPXem4S*$)Jn+YDsv8`?MtsN?E708kHm`Su3_Ymf7oi_X zPEY4(E0CubeZOqtHEC9sW9m*A<$L5)`KfPp>Ke_{%)_zvivkW`;Gx1&1HXCZI9$Bx zHQnkN_H%p9Z`mZ4UHgtaFg72>-C!Suo-A-V*~YZibveKQHhm!sdom~>8)ZhlEex^i zmAWjib5!Az1e-pt9=;I*#@$HrGK<<)q$!&WQFL@101wr_#q~}jUFW8BpPp2#*a^eU zSI+i>UX)AJQ_P!}Wym*LTv~}O3sc#e?Y&RXI&!u;*h=%&ZYW9vZ-WN_GU6)}2#rLZZG2KGfw za(=V0vbo?@7MZL%6Ql-t5*hk%!g`A9HH}-4I*Vmx6+-wf;vb;4&9%sQPTh`$BC@p$Iq_&qvjv&hH!{TMlrE2&)A?Ck1hpF}UWC|bX1ZInfhqu~Xe8ZVV|kE^w( zC%QfVC3T~$$jXf?esvIgq%BaFFG$0}N1wS3 zRv{+saBJihIwqtdPL+W%Erw129V`o(Fm(yhRn_IWkT0*FeGA3Lnx$R5U%}U6Qk?@f_WGdr6PsxL4*YY>t0*?a z^tm2G1rT^kg%{V$p%R`rRAoYjFRatoD|LhL3_TRYe?}@yh#tH=dgM9BB?zj6>kszl z5G#cE1|;5`KyMydvtxZ?*?>l9^L2AytK7nER;`sq6tdgbusv)5T9=ql+jDMU%h06Y z#F|q5XtHCM2da1>87a`4>SX%1di)}jh2B^+DTCfld&Hyl+%!9x`vuxk%fax~Dniz4 z1i9_piIzcQxA*sX46;3kCB)CTpl{qYuCz~BB_0fmIge<#We-R}3o`vUN`YT#1@6FU z6-KzJny}4*Xv)~O&q;g5PXZpH`@N1$OWiI_jl21;b*MQ#QSArpnY@ZlgEUIv-{T-W zpu%74J1sR@rt!Uz`5Ni1ifJPD+}al%4su>(^e1hAS6Z?0GAsCVdl5<>j$y?0gMt#t z%I8SGk#JpoTevPIE^v!l(%5d2=>?2GzyCVRtq`34TO;i?h6R}zQ)CnhFJ{aRclV4G z{Hu{fT6walNq3ZrlV^!;Qa|wikHm9Qb8 z6>|zA-Cx9~ooV{YG|85v$|o96{u$^{7Eq76dU7RV39~yeWMPiza5Ou{Dx=40ee8HV zn`WrAjdv_8Yv#d)ra63GAEYw7r~QQZd{GBEzRt?1Oy$(7PWK5-p@*kQaTU+FId7X~ z$o#=4g}~^_t!_SscKCCZcBefAo{J^0M`sBotH~;DliGvd*FLUzULK89;n^3LNlrE+ zEoTwLmL8%2)Iu2zs1s~jMOf(3cH+gpq0=dR4{~YUw^n~}5xRg5#c3>E8|AW1A8`A! zE+|7|uR2!ll3<%2uNpff!aIzi5*Oo(x@aeMz1qS+yxNZg)`AL^vQ;2dm&?*Pu+QvZ zA_w~3O|RNm4?BM>LU5d~FPPB0;N{@MG&%kLjEr}EdF19EH+jc5ezf`;h3mA+oZ+)2ve8~t?y{uTdVQI?K@G$sGR>+5zSO|6m; zW1XqKzP_F>3yY&(l$cRT@G)9I7E}@hDe);#x&!(YIxf% z_~+cLcEcl!=bl!he-?$)$X{Is;6suw5xBBejf|iZl`~$f?#y=&Q?%!j#hOm)n?AG=bI>=JLpfJ6IHR zC0|?MFNwrm&u?s=lpK|*&dUzgAX~%e0w`|Cj{mD*v902)z0cg0pQ;WWH%O$v;*WQ< zbs(~@4i@p|AjsI!NW`y@+44@RH|x415tqMx1}K{RS<5DsC0#>lm3@R~As1EP09D(nA*%)6=D?0oUAL4tNY-Q7X-; zXhB$J@0$z}?g0e;SI|M#mNv*d4{Xg$8~lqTN~NHGy|Q7D1G`xs&d38cjyRh)%d@v&3D8A2gL zioQ1(R=N#4iMOAwC_iK4Rs2qO2W>qW5i4M+`k$~RF)n6#TJno@wGcYfuyDn z?tM9A?zmJbb~vB<5b53SXU+m!4eOU^HJirFPWFk9;jgYNDlNIzt27z#Fj!<{WpL;R7Sihat*m(JA%Ep#CM`78K*NneqQh}itc&4CL_YGRTB_4ljqunRi2ABLTa9-Q_-s@j2yi=k?> z(FkCaYVHK9$5_yPHeM?NeD}l72rOh$<9(DA)$WXwR|Y!?xU=avj8grB?Qi%@9d4Ut zFbfS^SZIGYaBHfj9$zZZDd+mPZWaMq)$CXG=*7=HpGv=f*KSQD@t5(S%43mcqs)Q!p zl;Q=Qw$o+p*jf5mB0#H<{T5;t-tL&mvXiHaHuM+nh~M%yFZ*GeQ)Q_upR?lZ9c;1d zg4CT6o$b)o?eL{W;;p@CPsoF9hCJnN(qJLsd}rSRS!*WnkH5)2YFcTQ(pIwPS`;jk zLx5y!5sCpGnFz3|?3vM?pf2u+D$3d|@tt^3fSukTjV z@3DWO_{JgUfLXun6XA$7xO-t+{khFyT~A_`hF3X>jWar_F#x*# zxYbiE{6(uPNq>l~UW|5IwqDHZ@S+D{s!(3-|J?y-n;OMls=^lY=3N?5nymHr_oQMx z1F}W`0PQ{3$gBD6aq-9;scIVOQwA2=Q0-4$cD>8pwHSFfWVD9gGWI4ihE%`L&IM9` zI9pdztrBgk7Y`f0q5Vu6g4ygW@Jixsr}`$bP&evp_x61^^^EG036GqD%DFl0^pZAX|zT|g+@|d@zWp;l9n|9Kb@?*!`($8O||8lP+&)8X{Iv}O6Xe( zvUNmir7{Gdb@*Kk91B9lEqkK;qBJL z1J91 zB7Yh$U>5)C1)mtWR6|`(=#o zC*Sa#kMcw`Gm+QSY9SUrq&qQZo{>Kx?(zscv(2SqA1MY*;mg4k60T$xt1{vVJzxqQ z%n06;3od^^LMO#U%_lrb+l@%kwq{&$H|X8MBYft|G8T5 zi7PGxV{_mDUWwC8BUHAcvl7X;;-vweRN(_D!xXWol?Cr94LWHF>-W)d37s{zh%AD( zsqRduH9N^=V2jwky-zQ7%5v3juZd?o%ex9%wFS0@?Qk-907wmRKO!z3%ON=#nXvgLPSE1wD>5FR9 z`;F-V>s`eTtwKuSPAP%iInPYx^^($<;J^K!i8Xi&V1=ZER56|%TdzLQM?q?AD2c8) zloLn?a>#ieBRW0VXMJ3|E7l6RLCnPzb5)*G(EWb^+pE_qYMGD}r6ZbfbZgo0Y9(5e zu)pLy))vkAYGA7RsfHmK_(6oqqQdT_pmLE5SuD0j)6tAS1}|c!1(%%RUh?8@Y;`3^ z{-t`a5Tl&X@a|LkeSR%2dC@l^>sf7fB0UHh#^+n;&Od#eOcr%Vf1gI z?x8kBAUSUq%voyW5_=qeq^4)~{yN?DT6Ur1rEE@@t$VF02IudU2#@s)r0(C+{{iKz Bo~i%< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef4a3a78112ada2f3ce2d7c96fc4e7bed4deea45 GIT binary patch literal 9198 zcmb7pWl$VU(B{IT0Ty=;i@UpfaChDW76|ST+#wL$A-J=+1`EN$;u2Wgmc;`E3ziU$ z@9L`V{$9_Ysd{>z>Y3@8?w)yGdENmKyirkC0id9u0N%bd!1Fpl5rBb)hK`1YfsT%j zfq{XE4Z_C8!onuP!+QlHB_bmuB_bgqr(&cbr=X`KA)#fZrGL%D%*;$i!_LJ9=41pj zga3yFOjIBm)(fgk06+l(QGpo17u(UXUzAV)FAzE*5hDh_JSOpLy?34@0uf9KX{9Zs zg8GB=U>mRW3o;?aG6WVgi?E16Wb4oi4J8Ty_&+Q1|Humq3i=DgOb9?lML|RTAN>C) zpr8T?(TEuN1>|4&(V5;UJg)(8Ura;2+6e#YV7G)~XiGiD%VbSAv z{>H`@<%?48U|s$S(~LnUSHS}_dl8J}uneSX#~^tqpiUyrN3{+P)wrQ>I{jwY@8~Y&$g8 z7Vtq;u$GPq&IvRR@ZR*)_Do%J1VTCT*N~dHE+A@rqNMC;amnT`WGaBsJjO?3j$f*K zW;;hXkwnmtfa-^UXP5Hr3{FvJhzgP1I(kj(=cwMQldJ}smM_fXCqze1H(nWgP&dE) zPZlf8n3_?HTh48oKGiqWbR2V|&)rARDHiY5nG9Ya4XmHCAuzSr8kiw34*7|yN81&3a5o2BGHwa(51FNegYN|8;SJN zO9^(FcJPHu(_J8j+JW?`BR3|9!G+uoUE`>%ruNON2Py8qv53SPVdqv6qg!{r|MhuQF9<;DlD;=!ct#%aRKm98f1Ezx+^07TW2NCDlz*9#sg91%30ss zS-$>iRh(q-pbq~(+P|61t7SChEV4vIJq%%f0s)O|<2+1^4qL9_0SpvQ^4tvXP%npZ zoR15q-qCifwY6d)Tb^1o@n84yL^d(Cg{x2-%J#hy079q-XrNz*1ZSh@>auRg13z;Fe7(5B=rdqk7H2h-_oMTt5>T+qMWl=gDeVZRa_g&lOn>2s z5g(~%KpE=})u>jOOUxF>M~%Xi-7(IL-F{RGKSXS2C}wh4M}zc=EZFq`vk98@DWbtc zmOJjmFv?A+xw}jqnRG_U2%~1T`?qd-D3agEPKSYLoEUPuWos~OpTbLK5iTExay*dm zH)ji>s*qF;{nAoavrD}?s{3^3*xAjIB*~PoZZY0qbE0+QE8W*-H;TfAe>F-f9a80r z9y}-bp}<#k-mLxZ+Im@)Tkv>G;2@QXR??%OyB7~VnZEmBrF>c2;1?VYLdz0XWFuyamt zL4K3UFY*vSv%pK&#OZyF^FdW~291LnD|E*_9tMCs@FZje093aq*dkeR>Fw}J z6gCwPD8}{q)YFP=Ux9-1I?V$NvqL_w?F=E8aV-8z51#mOtj@D(v+N?eDZt2LIxi-v zi^ZAz4A^FWIhaxVY=Zy0?-lRk*Eq8U*f%;SjMyXa2Nb?IDIX@I7cKff6|+srfDkfT zgXU}OJSK00kiBWYapK)K0!OoAVP#GCi7D%?^zMu0Uu4TrcX|JwE1p(5s6Zt}I^(lC z6UHp31IFL~yVzitnPP%;c#qu*sco(&wIAjV&D)?2T|fO69?D*x+jQECxVMvcSqm{& zW4iF)>t?;df{8#1`#s=Bqc*P-Fh%Usj}Iv4_W(b?8Bq=vFd4XgHquyG1zsr%$N{U< zo7sR)rIX#Z!B>`z-3F?X$4r;f~_Qa~HDqG)*wcl9~P6twzi5Mz^-2pArKjdlPv?C&_D<|if zJ=$@66e+pP);cF2;PsP$--4_SS!pWtTulrpQ&~Fhh5J1ZivhLhJ;~ol8g~LYgN09vfD3)cyLiW|$@? zQ~OWKbgXkv(eL!JngIdO9#yij9qGllEnIZjWwY5a4sG7xMN4DNFoo{&P!u!aX8^Qi zZ?CQN!v=09J#1#$idXx6E9+6H)C3Xo>4aVFx24^?RrmlaMg_f+U5(d^#}VF#=rpS^ z-YZFvZ8HV3gD6#=qQu z#BzUT5|7`S6`PNpOfO!YBRg7A1U&3mpikI+#)G)sGdAJ1vBcT<_Vx;YR2jV;~oVOVWMH^-GUTx5*^6<)$xa1vx5sUpn?@a0|XDh*Iuqk)D|88CK1$ zEY!T9^w6sDx$qHzW)km4qt0#LXJ8P4<(>z8%rqFuff?nlO$i+nwg&{(Py$V9Mw23# zwtdY%sA!1W*HWG2F~#!dbnO6oy3AXa8kwrS`(zzAdu4igia-$^!8&i2aXTt{ugvdf zwtlJ-&X&Au^2n-w3gQrcac6OMapPwIj^D8&?b52=o6GERO4yYl#JyJR|w-;=y>C2>sS>@wW79-U{DD?9cUS;eyK7|IKEvPUMlr6#XRKg5w$TjHQ*! z#MLMkR_cma=!A}4b7G-I+!>nKi83};MVzlYQpg&*d&PIeq;{b$LkL20EH;5cKMkE^ z5z74E^EI&1-0;&(bTGE^KC~+fj{PzfQj_{<#QeDUQ~8IJ1F%{mHyEZYx_Y)}aXy-@ zYQs^C-`2xEh$!lBtqhUODVynDXhrAJ&j7g+PDzE%!89F`-(X*$+~2#usj1T?ZPx!K zmi%1!_RHak#7O78KP8=(w6qlWaY0v&dEHY8q{`9meZa|5c)G4j;JF?bH8{zRt8&I! zE>t6a;IDzDSkZ!34b`aHj*RJFrDE<*qk=4hsn8{pZ*zs&fJSKAxD$QZ*l9w3)t~RA zzaq{d9t$$ozwYC`<>;OP^X5W1iaLU|&@p>)S(;}+snOk20R)(E?m;BT43fJ|`v$k9 z(HbtM&{86Ns*;imc?LYzZWKOk@?1H z4TCLhn=T;+^hOri3%8jIGOVJFGXt_lZ!PLGZ$I|#DrH1OUS6q4eLe^=qW0){^K;+V zSMny$J}52?{kv0=4kf?jB@d2dTUDM|s${t>v*vAsd~PkL+@1!Fu8Do{J4NMS)MHNe z=!Usft2V)tz!0Ol(VOXRk7@OfopMxQ+k7u9TKSVW`a15aFRmvVt0=3kNB!7mI^uId zqd~qn{!St=LtT0E0kv?%)^`jKt>N1+f(5Bwynwl1P0Sva?~z^e(;5tu5Jx9dZ?Yuw zrpv`T**7L}=gDoGjz6iiv%8#ulhE${+GI6`A|l!?-1bLORZF&Q_lEpzUBz-wCz&1U zo%)J|P4=l&B6A(%+ELS;{{Bjc(Wm*gc50n4r-U8q+tgls*hQArFcTuJwHYzRIQ;4w zT46U7X>JDK4zeU=G<$QxQCnMt<>SW9UeyfyXSe>-BlJ_*qz5^tkkQ-3e*R)#HP5Dh z_RR z%8FJLps-j-+-R9imU9z4`s`13kjy`bbX=~DhWS8VB{V(f&) zQ++4yQ_JCAFQLP#)M0o)>6Sp>w0lJ|hocouTkip;$NV$EC~xcy^@ zoQvdS%Ky1#r+bq7miRZOCgB0S3(lShu&%3#{4zrN7G;`sXr=VMV#xq=(mDbmdWQ~w z-5q<@l!VEu)_+nAoxLTd)ukpEhOsh9M(UK+iB)zY!kQ}EHOFglQ-Zzt2sV6SRbCAs z)Mni!kEHyn*W)-g9;rybZ+eUPjruheSH@nhyn~)GCl03Pq@JQm1S6-C+3G8y5DqN- zd26c{c-{m*jZwbaYObf`$Y=0|=C!iZ}j=#sO)(!Qke-#zvlwPsR9?)N%rR&JJBh>6pGYnB;OIrR9DRKaV4OY*mLLaJRYPR#)9y%mBN>EPfI%j*!w_5s} z*<;*gZ$=1+I;&p1>wx&Db5D%-B*^g>Nh?IeKE;yKUAl=<89Kr}Kqhcv0a{2SE_HR^vh4N1vC2oGl!^&7ng>hff87sZ)iM@1A1g-mtW zIGSLdVbla8#L${Bo`Sulb$l3;eQcFq5G`FDAg0cv&VZUcwMIYMd61TRe)0Ah5HYcH zX8sJgkYdHFVs*T)9r3cQcYO~IWrh}>!q>#M;NL{Mzg>w_+}4>~k}fW;KCN5+vaw2; zuS+S*;u=G#suF@AO+G}VT`5<(=Znb5_#unAKsq#Q0ZRz(nvO&+pF{`XM zkyA}lxk`4x{jn5s@Gc{|ITZ_K;hlsUN?Kt*w#^r%2kh789=92bkY|7rMT7e5*ic}* z;i#17!jb>{cZJ`-MV|pO)19_A+pF+(1W!tNQ2$KHTZi(<4lE)*zH=-_T|-~|%6xGo zSVq)|;)dg*)ui=M9?t@cJ4o^h&ps$!-4xcqi?1&pH-va3K4iuQFWZ`KGZgSSq?$+# z^aKUb(&1PXE0-pd_*@O$+vbY~{2gAY-HvmXm+%T~;QeyfixG*O<9Y>Kkj6AWLlyqr zX5^UM85qC&(T3s9K~AsbjctaZHa8H;+t7|RyT%6ytZ_=fP&ou(cHwCGDc{%^dReCV zE>k)oUGyaxt7U$s(l}jPaLBowzdvqWzWWN-!&pC6+J}g`8KX7g` z9fZ?@2knwl(GF3y*FyQ)C$14LNY*1gR_Yn2CNJcO8MXXhY*{D~!Z^-TxVAd2HZg3f z+MN`y=*(1ow02A7C2}jZ9(~10mtgl&6PK}_EjsFL3#az&(GC#Emntgmn|%gk5;O7} zdL1r2`6x^ATCU;H6B+)#_Y?NIK`?!PA1J7SCv{M%sn7T``(I$Vbm>$e3 zhy_kp1~3%ziK)3~0`91{mG@7%jllHWg8KCuiZ%^->Gfkb`uU@9GvEG+861Rf zql8rj_+|IqxyhVhBZ_X8r>n6gjXma-w)$T~Ilja$N-|r~R?bs>w&0+`=IfPJsMQFk zC;Ii0Jqb>Asvqx)2G7G81-+-=Q^3Hn_?yOjkalNe?g!gJXC2{2N(&UXRxihrJSeJrdT}# zYL&Fte_;yvCXDkJXpkI^i|C?GD0<|H?S&J`oFZn*OX}-J&+{V6XMtgG0D|fQ{8)41 zs7^^FJWUrse`D6+_&1o!C-*@dzlo%{qdp6zVJa>8653~50K@#^Ahbts7&1+|3=#X>wfTyE!477^NsF?ON&aygeDd7mmKgjz^oI95;!S6!vpt5$0-=Q2G*&GqCRBnOXiBZ zS%=A`u*7o2-a(#9D2=v>Wv$j!lJ#Q0JcV*LTSiyh?j+J=%d%^O#H+kp>(y*qVdCsp zdDZ9udyLfJ1v{>NFd)vbHC-Mc=J^gpF($6h5AS#euv4Dk8`11qEl^Ob`s%{yy$*{D zXwXNcsI91Ju7!F6Zp<4Mm8RWwH~Sc{HL6_YLEUK!slkPaTjMg<^YouY1&7 zD$>Jv;ix~v~W2mCqxfS8G45^!Za#&ZKu2L}- zZROczJ#41ntfZ?=Oxx9U4dTyq6D{{wCFRAu0l7;_4C??j{g4< z&JdZlCYehb`Ps!;6LdZm}P{=S|i; zRXb`B?UN`t2g&cBruS7WutQq~^E2PPk!W-6^RFMn&5V@o&0-vFc{OR7#e|9P9M>Ax zmvROddj@QJ{GCWk_BIr zP_tQ1l61i|%DfU0U)%AR6Yw0%DywoBL+)N6vyIIQT6Z70?Wr{B8VYarA1%IYp&Ku! z(Eg|$xehRn@c@CvoQz%oO;09|s1`&asq)M_@F_X=L9a7@ zxNREe$CA@tii&hoxiV~UL0}0QadbE>u`L#gx3~eBaR$z}kur0S0QPl4e3x*Vov3lo z8=)Z9GxuO&pRY@^_%jN!2uT=hPJ<+Qawouj-X!xOwpFl5t}wF+Z9_|X*Oa!`31$C) zn_YM7B<2|x3ToeNOfh|Afr8v2y>puFJ7_C-h|HuQM&a+AeJ_# zk$otE#ToxDpOyP}n)(89kG$p8Pte9=v?Rvq8S$bB z#n~;R@rLMZlmf0Lb=J%WUPGE9dZl1^n~aIK*fy5rN1VzgUT*G1jPDeS!eycmvMz50 z|0C_!nUcEVbdw!gU&hz#*5`>MCBO=P?Z4S)U$;|EYhRfnU8<=I>RXaO0tw+@7(-@M zDu(*#dcFK681SG&-7YDk$_Y{rC;{bHpLo#nXU(*-N@hiZ;=$=aHZjJRaDqf(WkB0f zCmE=gqbS>=s8~<~`*}0MZ;s-)Z+M$(TwE#KMSI6MF=7i8wda=JmAvKQPaEWs5zrTz z`#wGFdmy=}?OtP10f&B}8N++UGKSRg`((X-%%SDw4AoMupx;_?Yu8se5A?e26uq%&bMgf;WqV0GUQN{-*Cx^7b zLkAJ9?sI8F()oH{$^0Sh!n?!P{B8=D->V&&CJDy4*SXOQ zrzrfPL7^=ohf7?M4)93Z%L4-@Sqc}p7k!%Q3R_gCVcvUEb11a2c`XY`ViKDL4`uS~ z5H!dRep!53xWcH)V4I(nFFtqtFe`;d+J%R~n0=8P$hQxD&_8;9WAbyS2O@^qErnPK z^D?7nf7df%lwPFb#=81;z{0)urG6)GqvOI1`fb^#F2~uR<86U_x43PGKX}J;Ub~ng z`9@0oT7{IqUA}T7eCYjEbc;(dx`;f8;i1aPKhQn8+fYvC=fob(Lh2K!n6Ju{ajiLk zN1`W>4ks`BkFRA@_hNCsETFx=zvR^F0hj%hEnj%j_|EsvQh!QaqP3^kfb+}Z_Mnrm zX0p`hf4YTSI2jSM1Dpgq>(lo;%2cj zA*h*2trm`nnmo+wz2o3|m~y4@?=ji1fb?I6*JIz9R#hW z4UM?M#2Yp$KG95&lgN5o^!uv9ziqQ7))OCM*R>%8d0QBkW25&s3cN`-O~D4=nZTIn zjbltlUqiUbzU;geucrPj*XO(Xgnmv&mAV!%{K-OhK2mUD_03nsm)FCx>DQw+0f=)@ z+KjV?x%NQ8PJjb62a!zJd+GYFvSWc#=--uz%NQ(Ab}~P;=JfVjb;Kvzks;{&%QJ&` zu80R~an&m4kIqlDFI%SS{$)W`LxWg;Sml=t#(w-MhL&~Owia#FSIJDrF$gy)aUS*OI#ht$ZY(K1k*#j)aBu6 z^BZj)uA5SH(r7tr4)!Boq6 z5WcsnLF^5!4nuvTWbfjJqtk9hR*M3nL`mH}nr08#xcUg~+H!86V9J}P6tD}D;z>=F zA3yNIo-HzdWX`3tuVj{a`E-~W9{_l(o6gu!y)nHu!^?IVJRp~Yk8$7p_A=;tx9V=Z zZ_6dU0-|UCn=r~Y)rt{Ocqb!|OQ1@*Lb#m10a~3Q=d`Bc!?vJg39J}2yl@lf!pAAX z?3o8Qf$z8Ps|5nG8m@qQp`W@iMHnS7D|(rx`D(ojMBE<~;E7RFthj@-&~x;Sk-!E4 zmsjcxNTIJz{zB>U!-joBi|J}&_Tt<+2kB~n-qxsRKoT2F`)s@H=BeaoXLIj}iX3an zggRGX3OG>09T@CNiHRcbnTr9}OBH#~wBVC&w%RCN;KvU|%H6pk-XJq(*%C_jqYLl+ zkJ2@oDtsS+S&;mXgW!n5OkM&aJPUaSr?;>wad3L>R9#ohhz0aQ?V@nnBc zM|a{0_ycabIyZ(MdWOEbFUtH(+QW^-5r6;~k)E#6ispl#vL^BE-qP- S4#EXZ+KS2=zRc%&^?w09f?b{f literal 0 HcmV?d00001 diff --git a/assets/photos/view/iss_window_view.jpg b/assets/photos/view/iss_window_view.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8f9b3c88511eedae54b81a156485ebc6ba45ab9 GIT binary patch literal 1311 zcmb7EeN0kuKq&6N+w#F6ldlgVVUSRtIP91fe!2@4I~vXvhu z5b(nYB79r)j&NaA1VKdZj*JrRl1LU%vH`+J$N&lD%MA5TgWD?QyaLmKlNGj!rdmHZ-P0x=8**y$um5$eiWTGgo9a%M4UB;)by>K{d_BjY#9s}u6ZZ^_Qqb=0{f zbt62d_@-~+&d>darX2-mvp%Y-S$|_-w@i^E-WNx0XKEMAkoe)fe%DFrE(po=Sk! zRCvePSe`x`;G`1y?viWf!a_k-w@NPQr=04QZ|B;U-X2Js?-{#s)GjyF*HV<-LTR;i zH;ZGmI`>r+Pg-~sZc`k2UgHP7Kwe_<=66#uE#n9FRa;I^cN*l!rdD+oHf_0WCMCl6 zV00p3(fg$2D{GgB0V@u68&LFSlHQ-fv;L`n0Pg;YS^xk5 literal 0 HcmV?d00001 diff --git a/assets/photos/view/landscape_1677_view.jpg b/assets/photos/view/landscape_1677_view.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de1e03ce9cdc16d2429459979ad4d10bea1cef33 GIT binary patch literal 11926 zcmb7qRZtv2v-RQ z@t(Jt(|x)hW~Qg6=WXe26M&;As~`)2`-cR$1OVV|6(9vbMnptHL_|hHLPADHMnS_u zLqkPH!^6Zx$0ES{@PPmiAO9mUCGaC5IT1cSDLpAU6*Vm_?FS$u3j+-^B@Hdj|A@f- zb45c%!$m{Gr6I&8r1?MNtp|XM3I`9kK!BqHz~jOp;KIEP0Du4h9MZpJ0skKeaPWvo z$N&`7f2=GH03Hqj9swC11rY@W5#gUPJOTg_7mpH&Ljw6dm4>Nva3TsmHD`X^G=ZdM z?*}d^m!z$Wk2JJquA#}?S|OL(<}?3vh~NMS|0~FUb&&p1w73BHfA|lL@ZU7x;BgV~ zC^_CsXdvR7QgP-xzpVf;{&D}NaRFk00s3i2IQln$8vUzo0pIi&0LAu-c;HdXF^CNr zZMc+~l9cHUF!>((nw6`}|H!l{<%-e$V3^Y_lZAl7d*w$PxX?mKxTl6TOOS?2^2q7W z-hEQoa6jqpeENs9eCMFF;m@mZq+ND9=8^9GPrD*^iB-}+WJ=01RK zip69|vj(RWvA;CgtR}_uVsq`OO<_}5`{AI!5Q%1J{8d>#!Ce!3m7RpCs1>MAvW5K? zxZ%uNxS%)^Bc>X}mXI)w3M{0bg%61G6?DDzBu7)Bc%(czc!h4h0oHg9kT{y>))8jn zV<@gC4@1PHMY2im^u*9qTwZW*Jzb`avmTT^sxKhqh8l!d`CAhIjDpCe{eQdxR@&bH zQ0wZyQp*j)U$CG4?0pVhXDg<6LrF!6rDmCZ^nL?8(Vbkue;|o`{X3C(Rr+(94ZQk9 zG-vLoJm0OKu!x!hw3olA8T?XUi6k2NB#@lwF*H0Ls3;sLwf^0VXUH}94R9v(O3*H2 zG}~9h_nSUe%-nn5EOP4Hf*@dqq`)t2^ECzkk?f=@EH&Z);{>ypphNqrv;x4Xbx}}9 zd-7+R3*mL}iM8h?btadR%UVH)BG!2(KGq*42b{w; zwP;xzxPGSa2B@-1e8If^qI#Q@ybpF<=tMF@bSo_~3+?NA103ibe1{btfEx^N{B`P{ zMUGz^Zyt$H+MX6zP+s(2Y^Mgyg>Q?QI-NBy0=pkMU+{!4UNCO;Lqsd-MCklp-KPJ3 zS{8S`v5gP!e{DL5;GxQ-B6II>KiRM*;^f~q+l zJtBIytv8M`aGKlxOlEKTp zES_`NMPPGYxkUqXUy_PL&IOs*f!Xm}P#{^;DBG}RixRLj1Y|62|4f=}G%i1R16XF` zX;3SB=K&+LEgVFX?Mt@3!*T5!5>tCkKL@VVP;^K>dYgB1{>~b#uVX<;|5Q;wE1^SY zrmE@|Z1e`WI)fIY=5!BqC7im+HmBtssTi0K-w=p}-j+;Gfr0Vs!#6)a^2L%I zF6fsnL9Qy_VNscVr&B=I^UH~RajzyCrAVWhK35tplcl@W9C2-COF`1Z@23gP1wn%c4HLnbV~V;Oah){4)gm!&%i-QMfW zKUvrjL2u;uW8ZtNUjY~o+Da?9z=|+39m;^p^~Ck^fQ*vxB+7C$ zvVxfG8q7QjLRZ3~f}a~KTO-sf3DlfP_*rCLg|+yyL4M_qn0 zu9J@DGPc&{#4-0i4h-_YMGwVd-*+X>Dwv6vuaeSm=vy52l$txKu-LpKM};r?ZO(EU zm2f^2cj#dcI)KwLVbAk*G}0@hcvdJ}b2_dx-+=_fpdry&uw2EF(pes!;6Rg3VP1+_mV|5d}=VM=F}^HxW6W zSr}3Z8b@6^9bTO5;QYJF=aUqEHcZ(ve?ir$RZCiFo8qh2;T``6eJm^RN+{PRcEl-Y z?hClkpa-=_0^&wkAk|+z?AL&sJ$F@6v>g{}U$@tUC(2AYf|4t8$YkZVGbKuC77|!D zA3B#EF8C)rLQ}aX&~4^NM50@^P!>!m#so>#@UtAWvhgVTn4~Tlgi8Jc_kOnU*V*mG z;SGK|9{3FSn4Ad@BQjS_woUv*-}_z1jPKd%asb~eVlEmZiA;{HiIzV=Rtfk3XB-!( z`jtG)K&C`@`)*15rSJF6=0e0aDpcFn>o(KV&y8Agoe_8FuE;c1uE;OPsx>7e+ zl4*zm%Zc&jD&CPv0UQt!M&5DsxN?g@g>uMCk5x&Ehb|kEnE|J``Y&Bc6=LO$M ztS!(#23kA%Q7G|nghsFqu)k_=tT;b`njOfZ!h1%Q`9`jOa;35&mTIto+==T0w9Ok% zV-DSHeeG9Pll=zi&F%QY5JE#wCpYRQR?$ool=aw#uL%p)26k-yKRVxa&ZJJQ2a=_J z-`4-i5FY%gzI?bsmU%g;wnOe))9xVHE_56&{^ndKU0C8w-?P4LhW*!4bK+j73}y6; zJBC{gW0AE0a=EtPrd&dH@MJ0;!+t_J0tIA;JHt50Xy-d{Lb>Hhcyw4Dw~kt%s^9#> ziZkxlDBvR`-q5~Y;oa8Y158VbW|u1sotHn~8Bso$eH5L{inq780dL&lGBVc9B;c&) zakdap?!DZw+bJU+cWae^s8v{YtY)nwSH`c2yhE7xa|sg#r9R2G-PcQf^ou`A#=TzE zEpzC~@)rf+S&;Avs{Z`Q*P&vsAyj`1i*tF_PUa~g+`zB>H33cjZaKgMaWv{hin-nL zpc`*+O9*e03+~77LS}h}xA74Z9C2J!_aPy&6Q`d&hn{qVJ(cTKXC)}-TAy0~61)q) zOr#yfi(jwY-CS@nE6W)tlH+PAW|nX5Atx97clXf4J^#{)h^&c>%r1agab9oaVo>^6 z6}yp!6wTrigii7Xh$z${uEc~|Co$yS|0!*0`EhsW*9YKMl=C4MX13Mb+VPXjx+w4d{|Jx*I_G&`cQ9PE6BU}BVy5y&Dh()#IcQf=KfEG+nQ*n zhT9BZltR`~5(zh2jN5k7J&ep`lJ44&obW`{5-;{n^;LOXX)a%!jpumr2V;Xs#vxfc zCKfuE0;iT;-iV}g6!+!`&hgF~Y+$9RowEXIRE`wFPv@CPzXy_r4Lw+PsOkOG(U!=- zmlas4*kNTZH1na&;!uXf`UZARAlS^?e5Ig{e~_;jg~BFbDlg4)lj+W+9Nqh;A#S?} zEF3qQaEP6W@VmANohJ;Y7`OF|;d#YVhWMg5jfaK;=OoACo?oXaY*9)6DX7>0P169_ z%xfy$(r})Ywoqyx|M&(F{9vY%$r_}k#c>Qt)a&3BiY7l@-?md9#ik-+RzW2gD7=rZ(L8$XNCE@R67aP_*HrDEL4cqVK zjZFQIEh5!5^Kj&ZVLabX&Mg*%iV}Z*vealOz*F6j`&&Ij_itJA79vt}^?=i?V!9;T zu#lZqfepr37Wi@)xQR`|xY?PJ*)3FU`rzzOvD(1;@Jr60Z6)7+n7Ku7&+k=P){%Cw zwBWp_OFf&6%>T2|&_Qw6a#^wCI;AHxqI~-Mp(S?tc$49P(RCN48l5yz_PMN}>X(2> z6v$@N1IpOzV`tkqm0zWwfh=&ZEjqP2;oQIfp+@I2J5nw9xx(;ug^$3(rM+f2BY+$7D-dvpFTS#k=w&;wZ*Lk^9m%pFK zoM^GU$Bb?}Gs{G_OY=CF%rzekpV9&+ItTQSktN)@pp_=N<0htFA-YvH-C_QSoLq-i zk<_fB-&DTRFORL3pHnusyNo5$!DpiFc7}&qpecX{ljfzvFCLerG{ajpR=t6St4|$s6{;7z#7O@-AS}#5X|SB0Lel zDllB?c4jF~K{BrQ&ZX4rprvSQ31Ov6qR|+XrlNpj@kv%`uWc1BjgWqqHQZfx7gE~` z_13cAj=7S80Q}-ObQk>K=9ThbMSeKmL7u=3$B8(Ne;vG^;Phh0!u}9bz7Y713GL3F&Xu~x__ zVJNG(_eF=xcdHvj;gB|p^VRW0*!l2mww_6o$s3P;bO<32klWI&D7va??ZT$gD48C` z&M1XDVAR0YQd-Y4r^}owbS|=NRiJ#?mf@EiN-RYsAr!br@8!sD-3BVIp;l98%0n!E zRT&186}db!kDS-sXMH{05YQF5D%u5aeUZHbgmj_~J`qEm9SdK{2O1k$?3Z}!IVv@7 zCYU!lgk58th^Ruw(JTk1LxH9W^pm7W%9}4gnmQt^ia@2l<{NboM>>IW;4A-6bCI}u z-sXiXBH(Jm=wr(bfHX){`?x&^rFX$r|0HFMYU_d{*8`@&*chQWnd8+B;M!=cMQjPb z!)5wl@p_l4^GDwZq!{g%*QUZW+H)d&xj54)dl&Yb_R!s3KGaJmiyvW(!oBRJW6Q%x zD!h0vwW*PFC01=M7Vz`4-=%@2p41>qpUePVWCRJ;5h7+oLJn6hr~V)K zY9go{%fBMBP>SsE$!5_*FJ=SNH8~gAuVcb&)8bqfpS8=M_x`Fn5Nv7D)T+9#U~+cJ zXaGm)FE-jhP|zK^MDi`Xc~nVWw-{Tv`YMQf5sluH+6FPf3GU`^b<-81pQ2XiEj2 z)RCrGAFGvowDAM)_J;gk!+?+ME-$h_vKaBKQx5cX#hX~klD3dP1DF@c)izR;rijkz zN4%nM7Gn@R9pdgvEGO(&IWNTZdo~(GIOKV2j+z;5FA5gJJQn!66u|Zq1YcK)qtulx z#eeb4Y~06B(2dSA6`~)+xz=-4UhL4Pk2pLZBEq=Bp{o2i8v|R()+8u|u^r-z>F5(% z4%h)%k%b#FBl)ara$6G3-X{^Xv5w`!&_)fm@e- z&}B_*mYeJHJtHSA1cmyWr`oJv*Yw#27YzBWc*p9!PO@@_W9!ZYt-%HFC?+qlbih47 zX-sL<3muDg6Pqzzpln+80t7#DNW8^Ru|8?8DrMD=C7}HkVY-XF&014VP%=9jmkb34 zNA85`2YV7DY)Db#4Vr>^e zxysw^OZ*tM@dH&F@~hq(W@<3@ycDl$?o8MyiK_06bu>l$kV+m~^`qoZ3)LpH#?I#pjDPs?;8pxIzeR!B_ zrzWUCiXDVI%$}^)PzttCIayC>(Q}s0VZkHN-yyU7c_Jj_%SN0;I1n_n)tBT!U$dd} z<*Um=86Ni-`v?s3Z?|rEsGeb6+ zvo=i)wT)Y&0X6j)4^2vgOTsUVh+zLKMj_%dANl??WNVj+@#n(~cj&4nbJ|{F%t9w} zhIpPgx;mfGEWnIQ4Ru2{G(lS}>1pJnI!!VpbS52S_T{5QsgFf*4htLvV<(MCSdY1= z&?{Of%cWuzD@Ha@;fjq z>3vbcp;<-ItNYj9J*y4LQYEXE2G=i$f>@klOu7B%DMC*_j)E6|w`U zJP}5-GNc_&THke@$X9JU`+oa=Bdf@CMM#ie*POWpy!#dW<-vo#gyZ>0QScUI|Hvjp zuu|sbJ{;jOi@(WeXRd^$)26!M)bhQ0;m~++)kTc>Vs*^g%l(gwg$Llgh;(R2v&tfk zg|ltY8!ki){ZL&8oJ)6_)7se@p%jeo0XsLz+HX?loFHzcK_((1G^! z1y=?)tkDOicZ@En#~rbDn6m1>^!|MVd+JHW@MW>>p5#e6EmBwLaXd|g`**WnI1`BZ zd%Q6z<0%C7u-rvn;>kLdLk2m(4YHO-o67{J>Y;mz*r<25F+FZxIRH-CFm2uA_4)0g z9H*TI5MQA z1~0I;I0Z7CI9X(lbykxktzB6|b0&0)U^`CPv5Nicj3e(k<##A_$N=vTeJ@k>oVth)lDy6# z-ZAkQ{fcLtg5KKk@9HS7Z(u6=rbL3M^b6@>1jCGUtZUJOeN`+Z+CmAXi1r%NI1rpF zLDy1vvmT?vXDg)?ZZB3Z{n;F8Fbwn=@i@y%;)Ia-|jL#5qyQbENB7_J58#ebYX7*btUX=jrhLepEjmOxYUd{ zguit*!JMP9DYA@eH%BPYU;2GOvN_{M6=aT!8bKK}kG()8;v`KO*&x7O`qBs#XT{^ux~{^WQ6t#?Xhfycu3XYY9CE_9 zmq|uM_k-xGTBykKC&~{}AqepuvRPP7#hXsOu55(!G>kxpA>FLM2ev%ROGV9F?~R#x z!upIW>Guptcuqsg=`K`!0;Ur^X)Yes(Gxf?jbjS@$mCx^0rfr_6`r&-YMU=MSx6Nhb}ouy!WQu<4y%_&F1e<*{I}Q z6ReaTO^>^RfigYB`*@9+g6h@xAp(ykdT18$LMEApd{m?7?+GxfP&AbA>p5#RB@?^; zVx1136Q0UQQI*+?KgD1|5}w?Wb)`0M=+l@sHl&imQ<4PYbaXRAk}Dm&3S^i=v)D-) z9s0Iy>@A}8lp5`{A>@jT+qVnnn+`rKXkIPb@MQ~1*FBhO{ls#8W;>Cdr|3uoLG zijjW67!o7nk3A-%9;1xE^_ID9fxj-d01)8rw8=JJ;)ZnWdilEccE}jb_=QBJ#A-~3 zeW8;?FJfTohddtOwI{H%t3eVp*fw{s3xt(s~ z9&nH>XZOc)2O9$AftFY25&if{N!1iINQ5g84BKtr?-zpcug4rKt7W@!2=Cka9e@BU zR)eI1p??n&S;Qb2d#0tiJlC2nVOfyFM~}pDZ7x{*1o(u;Ew<@H@2->o_3@xs3yO^Q^1J&Q@Qm^MTUYoQws=%BI#6jLLfqP9dTB{2EwIhchxmWil zyN|M&D;f?}9~4vz=b>stJgg+J_{RncVW_Gt&P%EMRFw@+AkIpDH`%W?JMNPTRo+9X zfyKJH#r_%Eon$0|WkH-Jig~|hm*r=!4Lp<|HdXWsd%sM={7Yig!=z46cYPfOB-u-NWY^4qq-ETkcIJ+V~X@r0!a!5yt2^g6L(@!XiyFZEtA zH&4Q^+}u2c;#sPeLI{)J1t#QyvMNN32|h`H3G?*0Nn$S=vMpH~ks;u(bH6dK*-FVP0f;L|g7hh~j))i&~1M+DG-ni$L@BteIin^KqQ4F_*T2o3H zk;kC7c1B487oaF~kwCTKkRsxS;H%?6F0s^o$K)e7F`BdEz-Pj%nqNUL8)jK|uIu6- z*^;{omWFn;ITeR=jTK-`iXm6}+h{-0)pA^w%Ft9;qGC(7is&<-?2Nbj)+rGRoU1mX zK)(YGt8^APqJ8Z-bIzigdL`<3iXod`mYI*=&hw}zKo8SGbCT~$)KmtUN< zka8Dm-3?4JcaCwqA^N>0ICamW7rovrxT>tDtBQlxw5okst;vNSPlg0z+>ik>MOhgp zNW`)a!`&ak+uu~zcjcu0coHQYhCX0|g+zA$aqu}FcE(1mZNEjE!7>^Z;nQHxD!!7C zxhuB@17h7ErnMt|v#@})ZsPB54}HwxYkpYvkpbQIK^G8IN=;FLV2)^6G(_k=IL~e% z4Pp>xZdN<@*)Hm}CgSh6A1y12;!%y+Z5)9rRFbflEmS6CPc@}T$AG#oIl`D1_><)QH-p<*y~MIh_e@_OWKHi=*)z!sTPY%Vy*5s zrq$6CF)GHSH+MJ7x}XjYgwYeAt1^0%4NCR|4#QP42Z(SbJgApC60XgM50NzU-g|9Z zk~%R*2c+e4h|u2e&<;RS*o`l3b1REpmhE4sYms=K_!z9I<4)&Ap8*A zpmAY}`O`aM>%|*jMa0y`vZuDwL=z>OM)Z>?*lHz*b9PkDHbQ#>(2p4dn2mi!wD>B z`UmA2T&g(zeq_5j^qg@D*BL^LSeCvxD`(>-bm=z>%zJgrI90TJ|E+}LEHubY z(02Q6#tK6Juxcl{MLfMSpZeG{Nqx z_~|wGiG3PJ8hX@)8~>@|Gw82|SwN4!R(t)~a&kA5ywEi)-Y}|}Zvf?kR@Tjw%_tc? zLG@Xs4kj>%!U#;JBl>v?U;GMZywrqK?5WrrXiTHYUohXSQHtGVXNiEb`|e%_k3G%B zy4A_^(!>sB%qP)*HgU+$OwN9lZV7uMNIp!`lfYlOBMc#*=eScTA$e13M4fT2}YxuC? zVScEK4PJ{#+?oNtvm)72A1x%f*!keko@O=OS*U>i16;fLRmB6_j$O{_kkL`W>Lm?k zQMkY>wXO}6dn_u`fN#L6a?&+3k#w|TvxeiDCZI{TWm;aInCswQ|7yRgRSXNLyW8_M z<`Z-I_enLC7Vg`;>ZEd&i;uTqOkvE9lf}Bux19RFYxJUvh}%m^S|>T;q&6B`L~>4G z@4g*>%u=vq?sgN`+_|nQwQ)T4+FNs7rAh1ksL0GfL{?huD%>TfanLMm=*l|k4VDNq zAlHl?goIvB$ofVm^HYx@&QurG?0yh_1B7KwV?uu1%L+s1j3{Nacj4)%jQL0Y2!$|* zH9%q!?H7H7oeBKdi30^;(eJEbM_B)A#Bco?<*+`z;je_nAG~9ujEK{3G(yIbV^B4* z@+)E5@*qlXjKYMWTty-XEK>*attvTj3|go_!gz5mLn*&Ya8)Q{>gw`q9#vvum_oBk zGrqAe%MD+SF9}cuQsq^5LP9ZB!0ghsMl@KcypuPc94ss#B-gOR9oEVb3PZGZbnX-# zWgr+^rxq1+%hLTNt2hw+`RIUrgWk1Bqqw8LRtMGHVWig4NITlAK$06YP*!9PaIMI~ zQ~OBMX0Vt4H8wTIB5 zCtlbm_4j`6eka;~g)@JyS&DS{aPmRN?L+Z$`?_-A-qL(*Ja&jRs@bT(*LWu4z(Q|w zbCoPRI5g~g&xa`u(-_U4WbvF8VyQ+s*mgyjCh_WA_Zpj9I2I}D{YzS9O()5j_nI~> zc2PU2!+S+LJWP0(6dVzd;r3J~5ZUKcuPi1Ck8i5$4AK4O;qHWJCa&9is;BwX+>r3! z`QZ6)5yFGjm*TdJ*V`Cvub(RIS;A^8Y;|;Yxw>H*bRd#7qMA=n)6GyoUeThtU9PMm zKTQ-+9wkpmYhgoo1ciQ*pz2%j^4PWkg`IShE7)>Bjm}E%|UM5rateM1kWh8PCi;qRo9C9!K+wZO(R$}rvJ1E&c#P}lUJ9Q z9S&WDJM>{cL7Hz|BKQdK&oqB)P#=nKYqSgVk=wc|>qUeO&rI1Bb1=LP;P}KIc56jt zacyP~6@zks59@?euO;e-Xy;qpR--b?Y3$`IG7qyzWpf0oE;duEIKSN>kBeBq$)}7P zo!NZu!EGc|@=gd)z4|XD zO`j41q&^qBwm_abftR2m$L8O8y3!RfA0F{)Oi2)h;s?W5TXC&4xsS|oebAP9>?PtD z22B&jkIfW;q?L}H=>)8Ck{A>d%s)WWqm2Zn(`d5W(AnI5(W8A|Y$5m2v$;$e(@TmO z+2Rh7zuc*3Q;|Vf@+( z8UwhEwi`_G#sUP&{9y-?{cQDve~JaXQdij`h$im}?cV_CYGiUVn+57JeFoGRMVI;X zehiZj>gkc@dfxm(*E0gzgkJGD^P9B^@m%kc0{HVL+<%kyf`qc^9TC;Khww*<1m9_# zv;ui?f;FW=eBd&@wmf<{PkQ1X>a-DVqXle1y>SurFAQv$bKgChJIvHr58nWa8;rcn z)*oGqQyh!N`%4h^vw^iGBP_2OJn{*{sN;G>gFC-WyVK+gQO_igIM#rF_hPclo*nWfw$MYR;99C3fXU`yZHUr_I->~8i1#-MD09OB{$lM@JGK}mD&Y$Kx(plGqOA@ z8kuvQ7?X7i=Y5C@UX3S5a{vjhjscMvTw!|Lg)=4YF~y0R=VJ^;hhs!~kAnQJD*S@` zri&jV%?}4Ru|t)!{h5(9`JlhJE#EFm*K56EWxB0e-Nd(NwqA1awNLG{9NhGcF#fW3 z7h9UCs8`uLa*BFKt@1y2aeMOzdgtsad%w?s{s}7fHaua*OrN>^HD6!Z-*0{Gg!arx zeX1nJj2aSW;6YbbxOL~8xS0^IAJ4?moW^4pVJ3iysJ)?g;2wI!m1~eIwy5w*tpbW} zcyY_!=9!;g31Y~x3k1>M$8vS2B;LlqQX3mfqj1-opSM6$zb2C}h(Uk-iVou(3Egqb zcA22VUJ)B#)Kh0KG2pmcP0!}*Ob#URJFYBGC{+HBPl^)VEAPXio(AW~&gA=5n9{~b zZr2~dqa$RYj=+Bs)Iuir@nZkRgE&Ix+m6|6UiVL@*EKa&rik&~v0d(ia<6p%+;!O_ zV1Ugp7=gf`*NT zhKh=YkA;PfO@vQOOoUHJNb-RiL_$glBqStfB&VdIrKhJS1~IcS(Xmp~(bN481n%EZ zG*mP^G&DRqQbJO?|8M&{2*5*yg9ltAz|jEU@!$~f;QkH+KmY(dJi`CN{eOmtgn$fy zM}b5A=M}>Rz{4RRz$3vSq9MW|A^-qz@CX1zJS2Q-E-7RJ8Z9d?Lhi^^6e3#blIEfL zYaZ>r8#-bc>nL9D(tpFr-~j)i|Jnb;1|T3J!NUQNQT{nu@czvVhxot#f9L-f90DHV zKWG9e8YDt)D=jY`=}6ktzv}=@1h{`8Bj5of0J}haYbnGeEO(w>As`A_ot}9WIai7Z z`dZkLyfFCVY`+qBb)H*j@dZqjVA>fsMb{E;R-4u9N${AwHQ~t$rACl+^r8O91z+6X z^0ur9+JV*Tsn%sF1v{NxO?YwuAb?ldOn8D9g5q*`H4_;() z`{0j8>hHY?5MG{{y_aj+I<;^~UtiD1@3?EmFYIp~g{kC4J)yG*=C2qUD)d!9y{y#7 z*iyPjSG_H#>0HM)myOMDJKdFjrQ_d>PPP{0O`LOwq8COgNHUE(x>tTErCA%*!}w~` zZuP~PQp0LSjl|1U<`x(T`?*@WT<+B_#aR6-{Db&A6};_Gj2aE{df1~d`lfMyL>Z%m zu}irV&so;|nk4wP_p_W9M9sfwe&3pOytMHq!-aaNJcpe`mW#oepXmEJNx}sFsbXH z;vzagtgGe|o?wC#oOp#f>(Rqu&XdFg$0_>rb(3qHAw$H4%a{DnVd~c56((m8NT@C_Z$(J<|9x9dY{`G zM`40%yZ)dydGkjG<}#xmc48yZ zH+)*R5!Ol=Ec$38h9)MQuq98H|L4zNfbz)r;C(E}_NgH%at>+#n=Hd8>G-euGuuLc zTr4@!&Z7jUZIx93WNb<^D9-mxmcXtVPQ{X(wHO}AK~nIpK4x@klFw?PuM>TCN)dou*1t&v0z^M^LQllLjd*;6$aD{Qger)%n2VVZ9eXr}S z0zgVT7U0HCSD%;0abMTQMLQ`c`Vuifk5IveEe@RAQqr> z0?Av&5U&QdqX1W%eiLO{V!bVU*KsUEf9H+fR2Vg6eJ?(4YP`rZnDS6Bn~!nHcU)`7 z_iN$=+S|HWXqZDL88~99#DW5rRHPc_$E6P)v!N8{Ge_)r-Y`y^?C5Y{qw&Ye_r%g; zYs+KTe^&4;CZ@9sE)=Y3-)QQTX0TRKB4tmSl7JBsM|$N|Yh+7d4gKEw3-Hy&Y$rxZ z<=nvdJc{oYaBt&hBqt}VFNV`T;!Q7>ILyrEic04kJF`+b2N2RByD=ucDT6cCX_hb*UELtvl zj2P-Y=u1I!r^?wvK*v`4B z6VQhu|B6`KweHkZVB8=V)}IG+_$LVgd`!w6NEuPYPGqMea}E@-buZXN3LgeAaC%ek zPgn_==nfu+7~3`QZwX)v9vw^d?Hy=D}bLCT|u^%{aumcM`(v3a-B zew=;skN!oTC9;>_PKvgyZVGt}EI!C!Ri_w_Ai4vnVVVd`CsO3$mz8IHui~sz;TmPw zVW!FwpFd$#o^^1ScZTrcO%xB2l1mZtb$qTG3F505^RokMax#b^8QrtlE`#HGGewE^e${b^`~-ygE-@;#I{yv@gbJ4QtW9jz?+*&Mcp~yB!olc=o}WwGvACk}O}J(M9ofGCE~$xaU%UZiE&rm_w71hk%N=S$mdTqu z$erXoWKA;!_UGLA1U8W_g6VS#I~EHUexx)9`lOCNmcd$Fy<3 z&-(O%{r53Xalukwpv3h98P@1yoO1M))K3#qR|N52Wj2M;$mym!0|i!c^G;UY&e+YXX758N%b%tPVfD!vrc`W znd@bSy&i?RB<<79=&>g2DmM>({-j%(K3rBe8wGbq<~Es8sn%oQuv6p@4o_^NQ^2prYIIqDs3a20H zA9;9&J#i``-}X#9HK2!ujekC=(kz^hvtxi*`FCMp@!(`<5A0HRfWzwqeAiz<0J$3d zw?u*8bH@`lPfD+qEh{i5xV#k4AKm2D{<xQ>2yFQs@qvIv9UYsFgbl*#3;6H zO-u@)rcKqJ1WE!WPop0ihFuPoj)LPwv3}O`#hmXP z5KgclpX@J_lvftl{Y3e@w3xv}*(7J}(tU)(avM10+t4Op;HdQ=!qSJ( zJ%+!4;Y?S2WQ{ITbBPRt``;tWQC|+{+!*)kDJbKM=RRiR!*h*Fz*n6S{$hPinory) z6NCRX8|u3ekJkBHht7+?vK1a^gm${xPIEoJzUplgC5#g(f08BDRzv^xjAl`&M(v!U zPV2n(7tne%*mWEsd4Qf{a}Tz`>^>j+!#a#UnnEV(cB1B91OE5kIg<-ZGXBJbujt)> zt1wGsf1UN^l5b0z%;l49^B5z!(4Szv;4KUR!BO+Is%`RR?9^(LO^+1B6cW6A{Qkk0 z1Qkv7&5vIFf@Cd=r-5xBmtM^3jN+h6KxrY6p2)BV`9gPwVhZR2rJvhkK|bCGUNC6+ zQ{PLH;;gew&CY30rES!qS@JjJ0S)ahfm9kL3zYGjcC1?MMk;WVA4d!t*(uP}QD;q; zp^p>%Y}V-nQJ$l}wOt}0!?YRPO3=NP8mC4oxsTNZ*_kWazm*gLPkf^EWi|wywD|d?N)W;rn_g$$E(> zxG{>Q`YUP@58qR#vMHx3GwRu@PGd&zBXFvHuv?F(4m^J0NZiMPv^7ju2= z5pxWNu!i0<_A=R zG-AZFky>=Dahi`uu*8gln@XyCr-0b#b&+&@M>J6Os$(YvW08V^;YQTlP>U3{w@Z6d zA}=(LFNoPU<#%JnVkPVAuogoChgJMCzc?LZ2A-?%E)UmdkD^o=2ZXq5ZE|?4dx5oI zNlDH3vhwd|`z1{FROKh$r9H}1vNYhtbKpqH1!uzt^|q%G#R+p}$%>pt2jp}rJ6%Gu z*WmQ^X$8;U?VEONQND-BP_B<9CcA%0FwmBv77H8XJ zHm=oFdh3y_?DbND=p+drH)Dg^2#g${m?ionPhE8ceAnEA%ue@ z$82{}Y;01BbOPDyrc5|fpM2jY>rAaKl)PdS$ zQi+gGF`=5ucByFK#BmpNr@_O+mClx2Jy`3*puUyM5O}u}E!LwS>glP*(}GZouV8wd zugOZ*3MR;S+q_Y`KS@P31*rHkUKTm0hIlb+_f2Bwxe-G7Sup;c$;y_EppsZ2i?JnP zq?pM0gn}>I1uAwdy_|Yhh3N&p#M(B*rbNvwSz7May}@2%(+pjmOqh03qyO^2+zw~r zE6gu*{!h;pB)FMFR#AfN&kx1Sd>xn)*xmo##~TYNb0KMb)6#&o++E{3cmW5?&pqOG zFG-&skLrUhIi+te53qU6s6=M)wFD!agJpgo#(%b1!WF8WX>jUUN0a9+Df3A3mMFSz zOh#|`W!cks1lotV7*nf+#B2f_}eKz(y8-DzI zC38-O6vC8|qipFgfMe`oXFF%8ghm=pTA$ zgex?vY#3?z%?3XREG05ao1D(!h<|Ud{XrvRD?kPfGMyAs&m}urHkHVWIB4@Jl zmMszo99^3GC8M+NoJTy&ok7v>VmY8!(cjQ@ngJ&Ma~}St7kKd(a4e$5=2-~ z6dG|&6Cr>E)fPG8cv07IpHqAb9A$V>#T&CDtqcxp_C84E#rBFKN1fKR`eb{Mp9bx{4^Q+c~FM=u4gfD&6oa~n4buTAY$qnF6_7> zU}#{7&=K_GAJg)HIhU{4BA1ppnj(QP!fv+0xIZ6TK^E1L7PNQX3qJ@y300(A_#6*{ z)EB=VH~9KNlm;JUgm+T*M7GC{@5kQ?n=8z*A&p z4r)ccr->~jc9Xlka0m-+;c)6|OF9ft=nVHoX<&OK+Wk?A48+s>PRW&!kNobd7X39{ z1YQz)`Hx0yulqij;t%#jktuxO)x_1xTI&AOeI?_76d0o`bmSWn(!SwJJ*2NScO%au zoTdF>e0~3JXqa8c@T=)BfZ(lmgHEJODNx8_qF0Ke3RfV~#D{QP^c85U8|D?oWRQSS z!BTf9AQP$f0e%V?9021Z%(O~65DT7Tt8P9bVne7-fT=+^V!B4PZe-?ee;r&JM6d*l8LCfs)(^~NYM}Le|Refd(P+k4AC zj=5c`AqPBIba`Ec%$fWbAQ}b60X$JEPYY-+wH<`BH|*jJ*_(EVw>51n9+5?^Q~!B- z4wFdlFm`%eColcaF}0{B=XFJFWOp=wl0`y~g)2b;@k#h|8Q)CVH*m0*%*NI%-IXI< zmn*X#;&>PfE0TmV%=oelofkikAV0N_B z-N52ES)G&QE6yGa_T%W}vrmUO*}u3(KSginJ8o0-Ei<42#5^@zwLWmfxi@C=UCR56 z7&r3$=f*%#^^?1{`mMZw{7QMJhU!9UDy%?U{-|*6*+glV~)G!SPFt1f8f`Y6;7UdM`Np7fLM z^{Mo!w_5>zhSLE1qB-go16WN#Pp>svQ0&E(kFWzZmv=w;q2q-xRmKxXA zuXg`wN2W*il3F7LQKYYUQ{bd;;k2{8xiAz-KyHG(S#5$qppXPFkGdb#->Lik;=0%Q za}8Gp!%CjO-`Qk|)sEC?D#^%98bfOrof=F?MJup4h_o!{kIBgaX2o5164D8;Hr|v-dVXh@RVBP21csfjao!iGc1B3;;{V zss9;A6z_h$6rnsepS8vsiocAY&60mlZ}taSQ(6~!hlJyAa1=+;V>X>4xC>D}>6?VA z8^vhTVRWcMLx+wjCu(m_GVxT%_9l^M{#9J`pE+Rdy6DlOoZ1D7ziU5m59b%s7`JO8 z&rj*>6Mf@%I`o)4_^bt9Fd_e_KQL)JPSYrWH5#eUI_>>?pEH@6(7}l|1;-haejfxh zYr$3&)&EGuSA?0n5G{wY^EH7>Sm#mK-JMERMsaGy2k#|ZHnN&10<1fR(V zpT^?W@l*TzpoAcrJOxbs_fC7S_hZ=e;)jqi>4dIf^YzHkskNzS%^SI$vvGY=9`5GX zdbmG4Bi^$l=K&p^6sCE_394m%?n+MwII-`%SI|P*PJ9*Q!imGKRR3$wAYh|UQ()n_ zNj9qjAye0V>1$}1L&KK;p$o2b*)Ow&X+oI)SL)dRPOb0*r!;`{_&ixVW2fTl{1=c-m$i zS(TH*9rQ(y&Y@p?6e*18v|qC?1*f@g2+GC@@fw#S`Hwf3_%rP;sZA*rYPzeO0URe~ z5Otah8BXcuk_WuZQDfp|P)hfY`A()g({ToD3`?zG@f^C<)Tn+XMzIl2H(k|1SNp(G zrC3AJZaAhb1KRaG(=8<-?rB;fkbPz0(t2$q-fKDBFDJg2a}ur~%^#mPv2k&7A?2nU z6>J|?;RV{5vDeD8;Z^~6BgCR=9>lcxvcpM{-9&l=%>$i@#7@eb(j!V^@8qYV5z?R6 z4-E^4_v3m9TU$BTE2RbtlqZM%8wRuCMYs?ca;0iUJ?ne_BN!Ba7%A@YJo_{vD0Hyg zDYJZp6hM+n5*ayGVo}Tou8FDB?Eu;2gse&A8_Fxg%_vw@aJ7DmPgmf!0sBlNR82hb zkLX{RDxK<&Ziz1=RkVF`%rwbJm9=GN^TZh=gdm%`eRgX|fs)q+8k4K@P$5uxN;U@7 zo*2A*nxaioA`y|^O?x&lQqva`(j-&>N36|CseQapFC6#7_+%9;1D9K8h7`&7D1slf z^h@xoXK?r#}+B9r>L-H+x#*|N=F5!0UCTciSlIP z5EO|-=k z>t!WfNXr4Fchf$TZULGD?O++U+W*?37VM(ZlD=gfYp|8}^kp2RM<{2o=jXDz5YS8e zki3cjCMEE~%3$pLBOGoA2}gUH9jjIuuAkn+Gko4OMvYfGzVimOb^sA&N0(UE_Cpy~ zFj&(bN938jm1f?9V0Wjczc1k|T8&$NnFC{0_{bDx;1!R84~O7XjqlX&IPnE3FBuz1 zDxBuGOiNNHM)P^_M#Y#!LP)v_qIqm;03g_|9_;%%$eSouk&a7(#wT^Q`SEHyZ^@KZ zorJRCg zc*0c9`1#-bK%07ARsVwgM|$FF<$5oQ9F!VZvUA*}p{EZ^HA23!*ntw=s1L^}=y96N z%6){RF)CQgA6blvRp;gVVc@!!N(gvyO;yc(=NWCmQt6KK|%fSMl<0f$L`gdob-VnhR}yhpdeaXP=s^TjA} zd$uRoH9^UH$A_f+ET&MU;Rv{)3Y=Pv94iB=)Kk*m6~YWlF*UPPSmwmoi|(JWWB9ZB!>H{#q1e;2ZNl_*KqOv#0h zIr~0q((ng)hn}0vNKzbga!nn-R17DRwGV?wHa{b#slYHi2wONU4c!nb&wTb#!92!; z&HK9Gplr%MXN*8Q0T%mCr~I39QS@fBNNqJ=dT^TTqC+lke>S&?tbl;E!F0f;so;owTYH@ajCy5{w&KXs!dYplf)G({BjIUBKAtC3nEzn>YL`p5*o)s+El5k7 z!lSQ-?sSng{TbZxYs0qF9pTg6;2)O06eU8F99r;haLyM{p&WtJ{-hdxlIC`^c4PPV zPuA^zs^>Al{L*EaES}P^nvTUn5oouIY(~~6S&x6r6+sf0s+&ijnEjoNLDDvwY{rpX?se8_rn0h(0(PR^-PkyeWt20 zu;_c`z2Z}gvoz-rG1J)4Z1*7TpNoX*b94T_CA7}=&V=o?K%nMqa++Q@CjXW?Y!qWo z_v^U^dUFEmk{eWUT0%dn=LPt_@9$Co75-u*hdysj1Y$$0&Ugptw@d>x zYHs-{e97S#umr|sXvVoHgMwK9m90)b*bQ?{V$(QD3Bfw2HEmi*sJ7Qd(3FuKNf0|H zikw>=R~I&A2vhQ@$PC|ai>z713%_T&i{{Aa7}^eIxoq*8O0VLwIS20rSv@ZpNDtRB zafkPk?pG`}VG^*nPHE&6B+37)$QC$<)M=WI&%k~Z{|X%)eX8THHSL}Ap;z2h=ta?)lO{NYIC52;6swyiB{79k_#+pOAZax_8FANOa|3n zH%5rL(lKTSNak{KJ{59^Y==sS1ZmSG9N((c@t0FPq@vbFoP6qJh%yf~(W3;9lU$n2 zvk{^g@U9qBZ2doI_s95*5{y-f!W-m+9xAdjdDz?gM9^}T5ELiDh_kaJx_%!Q3Y)4X zR_5$V4IU68Gs^M9QaCF7{{lud%VUq^pAP)4rmapkqoH5prs@OQzHU8-U-a}{A=Y`@ zrb+=W^bn3u)xO=&rgEmu=*zr*X+G6Ulo)XnyA2YL`vQPh1fJ`^8bPG9cqVZ13e^Go zscl`)0(kcbWa$&f|7qJUPAFuEbdAH;Xn%*dn@>$9i{JJ3(f`?WOA(d1@fdaG_@1g#$v|GMRl3Mh=xpIaX%bN3_& z2MQvt#@DVJ4bfG8++Zt=G1$cn(S!u%IvpkEBC^G#rGk^4eMW73Y4+x@RllxpZj@h< zeNA!j93?$!G^hFEpWf+(#5U-5f3S?cy;jzx_LEz63h)=;xZk)EA*I}@UcpK0P#iHk<|EOfxkMvM~qqb>A6)=gMD_UhFMxOOQC{2{eq*<%uS zg|`Fv3Ib~fmMvID`mk9}2+&n<6W8c@BO!j|B4hCvEu`mj<)D}I#$MopIBS)$_t_h* zX#!A5XXF*0spqB?yVOh;sbBIRN3eWmm=xZXb<=I?Zg@NF z;}$lN#|%Ym$o~uYVD2Y$>&1#Qif~(9m9sV$Fi74ib6t%6O|UlJ5FtnKr%x&)L9e&E zBv4RPH-*@bQLI(-2ll>_E7igO#5F9Npp4`UYh;{bb60Cm&=W&PYv$gtQzzqq3ds8r{?Xqtlq1S4nk)* zD_yER&qg-HdrLS21`8Un_59P^yYat(Wafa+d5;_yy-tts88mO?Ioo~8e$Fmb%mt;C zq$ge*#NAOoY@u`iG=#xACLb5VV=w!$Kyzj^A1fB$q~7O3V7aizl~n^n$Z5I>RNL_9iy$8)mNxi|)T`GG5R z3`EIYA7?L@wsY&U*d}WD<0W>E43r#>h3|(ragaGzTPRmc@}!^`*Gd3r*nb~P`<|x} zoyApXmR~Q^Klb|3=gHm)aphl#S~@gWxBc8}N8Zbm)`Wi;ygIGj!v;7|q9iII4Tt7e^nAw{dv|JmW8Dj5Fg(V#+KGX9o>(k%oHYXc16 zepyTE+S=|@lWieA$XSD%D5GG?GtK^2T5L5vzu}u0cOzv1tdlhcO}m9OnmIo<*S9bm zt|COBy*yPA&!v=e-lu)0fNfWG(`>2F{qjmIUI=3u0$|%nk5V0sK^}@3r?UWXktsem zchW(V-QfBu5&U5E8whuR>&7gb27C)$tm2rSHHpMqayxG=3YYir4SZj3rzLG72oQEH znyPfXu4L)yv*cIRsmZK|fvODy6C~d?9V^+z8xF{eHEU?f3&xwIBHJZy?k0sNI>tj3 z5R*npw%O*?)4A4mYAsz~7R3(;qPVA9?~D08%^H+A zWQ4r;R>Qe$%YQ@#VY59*3D1z{z$^PXC4X7+Wtxe%7?cWh(;!7A{@CvxK*TXDFs%ro z8@l;?+*};M!?iBIxof$UBBakUl8m_5gOE!t#o4IJ4FoEpOVLJ-@O5gm$}$82J41Ih zg3a*j7;`7E1qTI%Dw%0FTj=mw!GMV$$;B0H@3eYhy_p3#7Cak-iiXpuNU`2#3RgI( z7u!Wn;pNdb0U(|4h^_}&qt#W-FX6_G&)(kykB#;%DR_E4dY&ksG}@D~OxV*l*Lw~J z19yO=V$S518J($r0d~PF(5Cp#xNnN`Zx~KrdDVcMEy*g#v*b9UUI`eQH`^uqScLzj zq;8sU+WC5QrCtY>b2-gZr_6>Nl5TuFl@Jts`6s{%)~a=Wti*G%L(}~B`r2Z$i@l@K zAYg{R$uwnj!4yEjVCL*IcBDhiAa#cLPzRDIC#$}Pel;Q!k;lycC&$*UcUz6k)x*m? z4xpPC&|V)ll_Hx^Tt^dyQxoHBMwd0?@WikW_Vb*6)#(EreDtUq@hCq#KbkV-7}`pv zse@$F9S{xu3eklScj#H>Z8X(Ad#Q%Dq{hKjyBj;u{ zpE(rL6NMEXSD-%m{No7$*jq?4Ox73@W zK+dlbQ`c$x$8i0BK@N$i@+@Vf*1;A?&0+i zLbE(7NHbUfrq9}MeO=$NXCp;DNiaUzIl{{9f;N>nAhWvocJj~-Rg7#W#4ijd`;@AOp~$kYb)2T3ws2>x3F{YljVA_`$QI2U6%$KY~sWn<2k$8Qx)qn%(QP?cuz|4e;?l!Fyodb~0Dk-!lmslWxE3!Hy{FJ7;sry=M#Y4>K@w5>p#wc2=!=2y&> z=GM^TbE5`*!>|t<)GD691#cX-vED?1Gtg~I08BtlLNuDu#q}11zmw*mLOJ4rGsAa{?rnuw3X2n7IYI1e$T@xzohJ! zQ9*}xTN{m`kbBALw(N{qgBzB$luUjbK3&kSBtr0}CtWFtFWpWZMK~+||}A8K&x{!jJ19 zv@wcsZcGNmll>xENJ5SUbPB)>O!D<3^ECe0|1X{UK-RH0Oz+ZmC)}88)!hPNF2|sP zZen9I)4!+4Hl_|MbHQj+#90^yi23W>lctF;xjA=?z3l2nf=hu0F134hj1`{>S8F<( zcuu8MkQWJzHb!g&N$}!9zPh5Yf9pAOZ#L76T)1zdSgJiw`a^Y?km?{s#QyX0ABg}D z&sE8Zlfk{kve3{k8WWK1i8-fa#JKXbE_a}HSlfkT$5*$tgkC<&Mw$zA!%J0&oG0eg znw@&$U9K`Xo{Fao7f19?ZXo+;jX}G-(`_^QzC@ofJ%%L=0rPmhQNOkfUkq$`#Jk`} z%5Ig>SB^7o_QTU7`ohHQoZA@>!^uuoGYBh&qqA*+p3)h6DM|MY*P>O`p;tUHd~ z?^tF#{@SeD>5Dkt+~JM_f`n(?)H)>7Ktk;Wsk@xENv9lK9465fM<0HVSJW=`cVy7Q zlixqAnkz8S#!rt7`wv2hzOV(+zU&uDRmnIZ%zP(FlD5$|*Aa&uST2w`%`=ci7)e3* zmT+;D#m$W6Vyb0nDbg;Ignj=4MCO0#Gr}WRY6<}RH}Cj*-Y4iZv5j=$cap#HBJy)c zWkfS0P=)+^X1Fef3R$Kj^%awekwG{jcuo;B8=)`+tv zXC@|J=y$Uc)sJ_-$>hO|B1@Opt9C8zS2C0P@x#3@GhFH4ZDQtr^}WZhO^t1s&f$uh zB5g+MsCy^zs^Nk}IP^efJ__i4fCu(j`WGS+yac_82ckxs?^*JjRrwUCVqSNPx7BVHw9W|WLI?Q?Q3~i_!YCXN_B~%yAwy!(RJR82 zEf=>Ur1l!%E+v0CXzQhN7U~^lJ?;K?W-yxk8xckm)Vj+JPOH-WIhIt$2L|ZvH zgf|o}l%*yxGV6q9cGZ>I#r>k#b>)0m1d1}vmbn0RR(^g&5U-1&{2hT? zw{B4{_?^%ed0|GiO?k$1M59jpFJPf>)oU_UC5rQb?MI0xgC^_~ z`IV{oZ6p6o5yF?DGt#C`cP7VhMKna@ao$OzfbL1`)`LhR z>=BWre%?fgRX;c6?&IGKgPT)EG=RsT1g5X0iF4wd(>KwlH?^kH^-?n=F44VBS`k5~ zW(GqGo9kwy9zJfYYrSqsyI=2LvHIoo+a$1ls$Y0jcX#TlCYD!D`MSP(DfwM3U<$LS z*_4tm@TAL^f$b~j-iH%(RP@WPd_|xC>Pfhq9fh|@G{UXf- zbXS7#34fznFVHa7-slDbhZsd_18F8Jc^0dW8|)h5Ked98fD$)Xsy2=ny4n3yzV_$p zKi^~YNH%2H?oYCz;8WluO23+qgIF&!gwO8;s4f$xi(oEXr#Kq&ur)aLqN~!-D{Eu( zdG_0Og{b=|SB5XhT6p=wjh!mWkk0IKpj~hxw*f_MazNWV(ER!IzNFD(ECA>r*6B<5 zPqxfAwhP$`Mz_yJPf|7Bh^C+>P@Rn>YwgYVuP*x7{vSZkAq0DB_aHMHNNG~DAs#=8`l{tE3*|4x4K7SZXV-3 zA=HJHNln;BaF94T0)SMuY&|d2auKqWqb}vUr}7!of{0DJ4rR z4{r-+nV7Jj_450rsFMfL&}^7C^0oRekIk#+*yU5QjggVcS@PygyZUAwDSSD&i0&#u?y*KGil#1~fQ*QUgoucYgoK2Q zjEsVYjfRGbiiVGcg^o>xPfSdNPe@2YPDe>XN<&6SNX1M=L(jm-$Vg1d%E1C=rvo#B z{|5qmYl?=7hKGiR2PP#X1^++WYcBu~6^H=%0|L?m5b%H?JmBjffE)k-0pASp{~H1b z5ePs+e)CD=0uX>85D*C&5ebBVgaiNp5kLS$JbXGH86*OF9VNf&m#c4*6~P-&MU9>d=KyXDn zrh98XC6xj{_bx~zh@Ad@S4H?)uyM3yb&Odt>Ce0D-qCyD`_KYw|H#`ghqa=`3!fET z)=zm6yr|<=opG2S?rXg-8Vu+jqG{TRJ)yVj*|Sf58?Qn>fy8 zrpAukJNycm8>M0s!)SYSt1;ZL`gO`qxnq1lq65K|{wwwmN7YM$Vg>8Edz$nG&yZeT z?9{g)=FlUW`K7SMMRGc!g@V&(ji zc{8EN75HM9RpQ&~FtNm@8RqI-ksD`i~GKx45_8Vi~jBDm?pM$Nsjn=IlM+TQ$PK6p`Kz&FKb0%vLk#@T5No0n8UyMQdAe85Z>EhQw`_V7r!epB5G^sSX?lKFDrDJ+8tl zKzp$8UftVfkJZj0&K3hOCN&Q~(l`}LeRN;yf-pFx*i7^tn(Xl9Id4|}I_)4;a`w*{ zo!1x|5Tl7;aK5Dm`%7CoA+H^R{+5N{b1EvWVV;hJ2@0Ii7kUjc2j%`sGDGvob_v#RJm`nm) z^8}Sf@>yVHS*cV_v%{s#gWl_xiQOOlj^gmLJRoQK6^U|0W%(Spk~85URK1ROvV(Of zk>=HYP88R@h^vFIbi9&@ zBni+?h7X7C1%b7ei4pI4eqqY-wvP$-RvFt2#nIP8`pDX}KFYP6s>~~|1%J@3_2s?e zN#jON3Db14sC;L?p~czM;MfM@Q6A8Y0|>Kh1`V&=FXdtlJ%4%U_A%thyz0^%aXC`j zlc!Ivr|id#=nXjo=SbkjKZG`Bt%STFw?kuNE3t&H97UC^7E2!Qq>=@A{i_!s?g_q#tG0td+63wd z(6RSn!TC%jvfF#(y1l)^Bc*~5%-~t6QB9Tze_8F$ZDxV zCXVU1Td}y>pOBE|1G$s7RR6GN(fB6r(%DZ$mf(Ijj)O4?q2EHUZVHJ0 zGr5NEs6h1^fvUBSnP|v+8#DdTG=hFr{kyn@$aJ?d4ddEWB)l;-OR!y#`nCD>;8tqa+#4L`UKKi*~e-4~J`mey1-2q+Hfda;OdJXA}D8_uMW4NvLOqqZ2 z@KnXMK{#j6c)C-@S%)~NBlQ(< zCA)`F@V z8u!_CCU~OtyInp#RJvZMKlj~cJ<(<(bDKZ>Lq8gZAUujMIe{Cd0JMlO-{)tS*_I?H z43X>~^Paf~UjII@k=dl}9_qfAW?%+N?N5FDqO0YZc6UTin&DfdjK-Xoeq^Q-)cjS1 z#3Ox5-{%e%b1=FN0Jnv*&tN*hQ&8ePW-#)l1$obZ$RwhQpjk@=q1{PD?eHv6YUb zBhyx0Vz0-*R>}iRiK-%hpCt8*>oai;n1TNNh`oeM`Js_ja@2RpB_F9{BHZ1%iC!n#4d>e1Hfz!`k5u=SiIb=LM1w1+uF%f2!M<1^PAO(*z}6p#6nAi0Xc{KPfHv_0wUHx ze(-g1Ps{HWTGCfQ8?;-WIR}~-K5D2E0Z#LK(a%a5StCXn^kE?rgZ5Rx*axb<4+b1- zEqNp=;Do+Zx@`qm#x40VU!w%Y{HP&6(@FO zilS!XuWe(9`+Om3p1jB}`hTF--6=_2>UQ?Z34)%;Dl{Q4gqu6FVGC?l26qYfaesetjI5hG znK}N2Y>(A5+brhQe?NTCMPXKtDy4E_;?Jgn&1Uh;r8DsgPYWcN3jHL-IEB9gRs;VY zL|9`>n|T&uaXzU>bbAV2m3)y_tM2@i|Lqmfh3LtYrn@Lt4p%_ zUnu^~jyNSsui5eJXg{(%JnYTcMuWe;bKKqr^gTUL&M_?Dr(~WJUUWFn@cmMyH%>e9 zZjpz(Hqrz@TMN}lf`}B#jp2JJZ@<6RVi!E6Cn;UW zk=#}DXUORxZa{FKduPFQYv_i~NvCZ}N@-`#otdB~@4$dBtu>r=2w<==cMC zys#_hXcyF`$bl^$5_GZ0Qr1@b^EL=W30+8C^5fIVcw*@*K(FP(TD$8Xk)7)+z>qD` zh)y-Y%NwPqry4{hOjAYC$U#=3l8zh^XW2wrxU8P!5}}{Rlv$#H<$-`I6N{HRLrj0 z>Ofb+qnofP74tEl#!HsHdkpv83-7jI%c8~uq3*YF2x-fJB+b22&Em50c^o?=jnI9g zsvtHKVdGGuuFeVzPDWRRPnD^8-pEf5)mT-1-(UE@B2CG#U`t^_@_F)`en-lW=zj$h zqgRNC9BcBJL7G;7!x);r9Fh%|-hFel=X0~8lX(S{r$g__3UhY13{zUCifC}3rMlPV zz;gioc(n|0xXKo&Rx{vn$cZhUWe=jbc!$a#k?HgJS62yhHvk7SW<9kCA&^Ssu-axy zQKPO+NxX5dZ24SK=w&ya6L$yk&Q%&I>qZLG?}{kLYPKZuw58G6ZHgxSZ}IR?yUkTU zn5KBpNwQ>5hO3bp1L2YsZWO7umzz%vIFI_n zkrsxNcDWZh&e3qyMe@Xj^N#nYuw8d#O%z=SunB^irzW(e_hGkD9iN`9$)0Su zYL8fW`0>f9{yR8WS7Zh+kM%B;&6dq=!E}H4@#ya%rPrfjWi-P*j2COns$C$lKM)ws zBo_5d8w4*eoa0g>J&*@LMJ9OeFHP_g@^7t%&k|oUhIZ``*R>+~x>xQOBV5Z}izfr< z;zlPD6x8pXX$1ss9zrkBQ}rQ+PFSxP4o>qb0!bI?d5HWlxJ@NhyB&90P1p6g|~=s$Pr#{{9b z86v1BDn%b2nH5iRh;@u1521ywiL9T^-F|n(Cw_mhc$tu1mZmgSXp&g6R5Gsc!QmZU z?=;z#w^u<#S&f+F(PPp%X{yi#VYlXp9g1XuX@hr(Vv_K?I7&z!4p&n<1OCW*h4~OI z;LtGmVyWy=xKR8RVE4tMyHZ%$^V{c;pW6j!Zj{erPwqz1EI}M<>Xhrj{vpZw> zFll|J6CR@D2++&q5pSRmN05Xx{!LFR*i15(8?I}fJWPe6OgzE3&USe2%6%A=_Z&dv zF&`tVl7bL0HzpN5aRM%Iq@tD4_)zz(eCSZy0E0f4H!nAI?HTd;w-)jQF|kK*v5R5g zYIVVTfk)TsGyn$8<^p#{f@4S~SSU)>Bbo0GR&OdO_Sra>Ppy4#DDM^EEGK+1(KR`K zl2T?QLjAF%%q3gjDQRcyQDM3a2JjzBF<1mooHZ}~ZZx9ozM(Hk%~1;6{JgN!X<;vj^-AKr_1 zc~ixwJgKiGI7pRV61*sFA8PLe);$HFOUxFC62>N~WDg?Rkc(^jbcgapsMpgFG@VYI z5%9LGpj|%b;cXOI_*ihINYe$~(0`YlsmxX0Cv#=_$So{fA)oC-F(DF!oc7aGb0m3S z-z+1inB&|8k{ralhX?@H9Xp*lrD<dWFahbzBhR;<{)ThRinn{kxS2s)j{a2U z^snlBRVXbbn%H#nB~KN8Z8Q{Iz@Ny21Wr?YT%fJBulVhd?~vj*CB)y6%Y))Wmnl0& zPswLXgIoEn)fml|fJgiyFLV#qp&o&r+VVA7`xUUqPWdgFuh9m1Og5#;b7hDonmgvt zun`+Bw#r_PmVGw4aKxW*Db>&$WftL8-7LZytPoD#pqRq{IPt& zB&a;H5G|!k@b8HZEnBfoCQ*PeB4j%ly@j4sGQI>E?GeAyLb38gKtv<8;^QZMF2o#-1VKcuji7rs{dJ2Yo2Q|l(H4D@ z&Gf&j=Mb<~K;UH5$9o+GH;%wx0}VBUgcMg!^tUoi0o}@RwWVgBcxkm2wViUIEqQowJF$7K8A@5edm>=b&@&;zRpq zHl9)n&>Knm7SIcoL?B^&nJ7Mm_UV+mKOC zqC+qJ8O8L#)S96?X`u{#@?DaUco57k(t71$n$qdy&@`RcJfTRK=JKYvX((5Bk{cW6 zv(@9JB6A41H~maKfHJW>a&tfK#jaO^|ME|jNi*w<(+7Fao{V>^shWm+PzRCOIU-B# zmaoW)@4WkzVQr+6rH3k+{jS5huJ`ecb!2@{zfD8YYF9S^EA`Y~2a*DtOC|ErC6>Q3 zEsaD&B{X~^tWCJlyO#vl+>&_WotpnNG*9jkfdFt#@$P^6iiafe3iCMx;2jY$h|ZX2 z*NS6JNf1hGw8qqj6P;8w8&6UNtQF&4#N#cU>HXyL(s%ZSn3epM`Gt_@mmB&?yTG^{ z!xVxPluwk<>9VWq>a_hIdKG369=xb<9KF^pRo%D_34Q!V^#K4DC&W!VK{&>T|7>@A zY)H3jR!O1l=kN&Wu+a2k=NN(MWcVbE!CCVZ4if*+V_K#2n?iY`CDXOwStF7DV2aeW z#CYZAP>)Ygg#o3Y|4UgP+>7$x74S0T$70L@OGi0W*N>+rEOxy`yqhQ|whmB{# zL*pd0s#*EDRnl3K78U(b7{f940QZJJHSMG;9irdTceJVAw;1xvIMdM8tiQ^?m)Y(+ zF>%=WofiP7OGYtAHpRM0>|)RV6+qN4XvncSjJ6G2f>pb?kf!h{P!pcr!{hgfRj0_T z#xJ_rP{BFUWreHZBNQKf+eeM?gdqSdRSWUn1p&}8oabQm`?h+U8tx=+mj;h+xu8r45gRH4DcBa>Ov@!59(yso z9<*2|Z`&~HCGN~EE^UJ%`5%D<{RC=?QHvPZA0CiX5aS8BD3UF}4B zvVG>tWB4q&UjD0paK|g)jtp4_{R8ci5DzZm_cQ-X{sZczXG+9Tj5M3vsc`xxAM{XL(+3 zFfi~Gjl(O_EP1#q_v2hL22B9{9zJhre56a;QWF>;`Hk8WI3-|NFsj>>8)WZASJ1hr znG!ep{!>8);bnkDwj_)dHh3b!Y@?ZU%~jr!9kA_mNn@(z_+NH4vkU?KC$Y9+%}1N@ zMr|<(qYgQUo4$Ar#J6{nFWhGiR#Nu`(W0l6@V-R0Pc2-9mMfq#Ii^8ni6g!5le>1? z*y9kc_6v=zlD?bpg)m6?$=SpWva}0d@%$FCROyqf8I=>*T_ch@=l`rFT8>yz7vGVNXXVYczH{^oyi`Ax^iV zucBwwGj5Pj7h(atqVZK7h3u)lhwEzI`)l?!8|vRQS}`|?D=_e>udECJ2}FAGOMtUj zk`s3?nFMOn5J5GV$k0`9e8a{%C811owATC`K^$GpLHt|;e}Eo-Z}@;RA*RQm(oxvI zi8MND?k>SLR7E|2Uk)^hf(ny~CQF5~gVuQ7#a_Ub^C(h`No`=~ z;p+tdu;^X*sL)WKE;C9>O&z=oJ@ZUMmO*p|C)Rg)HD+Xd=dEPtlGqF?W`y(x*oOvx zkZ#yJ9;^1^w{|hkE@sAl!S(d{Ln9j4)zUgIvma+ok}h+aE~25w3i3*AlM1T7RY^!c zBi$0lF!K@IycPROCwEv=%y2Q4C9dSNSe&-HlcQ+XP!tZBJ5P>g5>xxJOX7|H1Jp0s zZJ+gJlAjyB!;=SAl(}MR{#mpRSxXA1hmEP_Cn6|#1Eo#ES;Q(U+Y}=Wc2*?(=WQT4 z$LHXZVlF|~bcQq28wCr?{QnBbt!g2VkZ8hifgFNUVd~Fp?lTccU#iYwZh5T(L`eqn zw`fmD%+O@G86!kd@+P62V&4qqPs#E~#RegS5)-!iYj5FbLv818q^=a+zBK_{CeACAdX}csC+bZ2r(?e)awavoZ8Qnn^zOLa>5SI>7*sT$t zrm=1nLvD}O**M#J$eGY0bP4QP`KQlLHsC@a z%8O8hY3Y#WoR!I#!mHq~=G7xYNQd?)ZH~mv?Ar;ICYIpQx{dq$wG{wL4vEhfGq9bI z^DatLo}PoNITD__9lDuE9$M+dE9_lR&y;~vc-I`BW^xn4Z3b;9B7r9-d+ZYVBGT_b zEor`1_y7J*k9V{W*qXMF+&Dihs_?F>udvkPmwu28DqjB;_LdWl;eS;EE7Lfq7F zcWqNd?B?o+!Fj550|a3iXvWOcz@qLvva-$A&Y%UaE0(`Xh!eWaBGIA?F5p<;9`oHk zaIrZM?_%X1q0_KmM8bTS(S*>7Gm&GWRjy=3Le~$KK_`WRZMn>pS60$ zEi>IQ`rP)Q*U_4~%Bs5o6o32~zP0}_-`ly={mvf6-+&>{?fE!u;HRt6r;-Af*oeG# z%7VWZ95DGlaF?PvRJpPv*gh9*#o&;>0vMt7`Xfb-S~y4OoMs020UBr*J9p7Dp?As6 zc28sc8D{{zAp44V7%oLK1;xxUQV$p_q68NCQu{VDT4-w_MmL~Y>Al|&$mkmUi&_-9 zD43cH;tba$`~WOL5JPz>PG~Ldx(%AlyiIGOde9~|M2wmH!$71^9f3Jau^2|IXf$wd z=r7Xnxo#)3Uo6BCsy>0H!OZ1{uSA>X#N!WX+{`jmhHEgWvD_s~g_m4^KkDGwpEo_5 zFJRmb!rAPUTyM$5AhRJ!!et!^4_Yd5$UN+dwLtyD|C#iXpxb5mEU@_2N#CL>*LgS} z>XN+IB`_?(HI-^X)R-Ef`(CC}`}P5p*z9ztA=IEJcodb4%wyK!gb{#VXFW;r&tJ^& z{vJ}_(qd-Ye6c(Nji7$mVwUjM3`6IkWa4Bx(8)$a2X4*Jnv75ig`RBmpOsQvQ^2YJ zTdg$dA7V)hl*=6X^cW=!+706~OK~Y|cKUME@s-Mx#~`fK>OYIY&kP_5l#~a3|MCsR zt!Y-yWCCxtN^9lG0OdYogkNpKhrx1M_6QN7C0?b8caI14Vw@ycXMFpLz+Z@U>$b0xLxv;RgkY1K2%+te=*aEBJV zG~HMqMHJ!R43ElxLH50`fF^j@rL2u7VNrTi zUQ|))fsd{U)ffEoyFXe_Q#a)+v)}6joY3G{BN`QIo=ITcB{n&{{gH0}D)92oBzAw42+kZ*1?_6mLLPqGddxf=fRF#Fnz)LQkR-TOPVFLrV~K z0;`SGc|=qo(ov0EP2x-MzES8ZZeYbAUAKHQM|qY*LRLD`hI7bi++NX)T&_MVPo8Y1QUlD+E2#FE*}H*x{<2aMC% zMfAA+R66SB1bLWw+8U?(D3^Giz4u=2D8f%(T@H04RB+sJIi?Isr0bFQ>|7zdBnFCp z>hAnWT3@H)6&u#dF|zc1BI5sw*;VI^>yfD+39~2h7;<*Cvz-vSO$A^63VLqf?8?-^ zF9ds(w13q6@j*hYeiRoX1afX&hUXANb}YV<$NYL9W?Pfx=R5{FJd)*h=cNts7okfA ziA+hp0>}dcsiRD^DA;;a3iq7%V`*?Y^$vqL=k!ozx$=F=lOGD3qEY80Qnc?e{Xj2) zge|QIN>I0E7fT|l7+k-wo-+#@#tF(aG9U8D`=zvH(D!Gw-Y4h#qDCvp>>*CGeh^s`66R!crOz+O` z2>Lfij*HuzO+I=QNU z%l{!rh=^2WwFP_=O!d~H3scok@ZPH!Beh8!zwF^h{hH7on2bVkaUe-lv|fqD1nT)r zd!cKka$SF+Pq`xvfZ!}PEA&F_Jw~Jte#&>GQu)bHyaLoPXGYGLxaI3ahF4B*e&8;y zDyORlAfZhu>%6_mnx_4Q!h9M35sWA5`FHBRoyYDPrk~hLQ~O@3-J}mg1M44b<0rn$ zZ`g>P92=7jRf;d}E{!Q9V?y$MMS?y?+2yF_Y5XSdj3|Itw3 zD{4L?ONgc$w0c(wyw~_--3!_o{Y#zy-Z!!bX6*x|L!~TVzvzAiNI}_LDEyiAX`$ik zJ5H0F8+jCCabW~pKYE|p$eOgsQn?N>pu<{chD;P{uH17wFp{^G*u`Su8GGQ5y0-Hf z9AvZ+*iYitpbWAaper1Fk=Ub(QD$&zcfP4OD#D@pkz(oHn4rM$KJUs_y9FmCT)Z-q zC-Qgt=w3w1{mHZdV{MWSK}h}uyg9gzSB`C$mk?VP+ET4WGDl#_v*f{==r3z~*EqFO zLB2tiWZ8a+C3PP1V=lD(zuun(I(+5q;Q`vBhjRtRlI)0nU&I?C=m5v*p_QK6Ys*D= zpCT8n$d767vd6#$Qe9bJmQz3HuZFxTsf~A! zdZtq>YeqqIYZRjzXrwY5yeihQ3~a_Oq{lu58ox7POw7byDeF)^urkiumPE+B1CUg&NXo~po1lZo8kHjQsE zKxj9gZD+vfUC7)3KO#!op9wtYJK7n2iGd!?^x!Si9OEqrzeUMS75t9 zfQ^=UL28W|hE=I5696W7r0|mK^a@A||G+98pah>ths+k zh<;jp2F*k~0jmS&t4TdMUBcpTGYsNrqgMvkP{5U2XoiS!LA_=Ikmj>=~v zTBf$(JXCJ%I(#{f_M?hr81(vw9vQ^Iqk@-8wvO)^crrxUsl5g^(49g?exQsm+fvxD z!7Q@OD?gc%ywtn(8?+259Zd^Po~li5avQh{USh}>ysEmqESkw;0PvKgFY}YjYRd+wzq<4W% zV_NtgXX*IhOOeGw*S7sh#&GJ@J*=%zI8CTpekp0W_L@?5uo?FFVu+JOyKK)@4 zt*KIUV|=L7Oz9fF1n=?@3GrMK!tY4&W2^aqe5xX?UeE*}3iTC9t;`zrL}C z8Y6O<0gIV|8C1~SRJINKIn;tR@l!8<6I9;Jv;)0dN}lq%)rS3tzAbFBj>TY2_leTw3b^t->+Ap)VZ z#h5tJ3F+O=T9RfViEMGs-wkP9n05B@g-L0szpRBgrE&6_vg2~2rEgY77X=DC@dhxP zY+V)#dO}szpf)JOnoi1T3z@p6;~T@Z*y#-q*L$;rg?5I+i0FgR=ecCnZ*G>KL0FKm z$>;0p=F>;|A@rJWz0<0;XmCZo_V_8PaC6)3PzlGxf*=fbQ?nmPw7hQuV{u!9)LV@# zduy+vvlv=*^;>`Ct2SUWm&hK0X^P8u(n^huix-BAtbnTGucU0Tyql37GauKPnvB*4 z&KBTuL52}yan)6!0iZ@uuX`Z*^OPC-6;7m;n z-|;g&U8wwQq*R$qvH+aby%dqZmiA?xHDw`!(_b=y7GaAb3|uV@zB>GWa{;3MB6WZChR9gWcU6d?)YC1RA)0ryZ=xa zUC&T^V6j6&IV6g!2!UsMcSw$zUD zGzOv*=|y@Kc|Ev2=rr*uVH1EiBp79c!;}i2-ujVwy7)UN~+na~$CH_2X z#?+ZCBu>Q+68&hV&G_4~|NWh;ta)-7<6lJwWP z#CH>*sd-rX-s#AB+c9ZZy7P~evx66xK1N&%=@v~;2{>3s+5Py=urt4%+Wb<9Ql|*A zD^@tTSN9?Pra~fh;U+gO>BHzhZ3U-omd1&bZlB8%#4A!e#SHxY>ojigsQV|M2mM9{ z(nWzt_nRQ37U~~ijYbktikElZqX@}F7@<=7wS)Tln^kHB@`n+>OuWak?0$M|z5cU3MB_W}#YMTH>n(3ckh`0X)bYcBdw zlg;Mn2$W=q8u|^rqBp8#9`4=+SDx$6q~5masSav6?dR4UAp3SZhz(j7F%-j(0)=+- ztIYn#Azgn?mm2$~KD_udR#My~7*5!juKk*njp7n&aaa!q_-eqYT9fF>17RfFD-YcAH7CNBHriv~Hm_N(_uA4Gi#|1C=frF!f-Qcr2f8(&ky^t) zdOPC@6l#;3eKHUEnT7cfJS%=X+V169nF5$s8mP7%s=xgjd>$L|>Ek6%!}cFd3D(Gi zL)pe6m-P096bYmt4}k`I5P>7;J;u3~IxHBj*3w!7d)tW0 z*#dl8!opNHd?+39jwE~@j0&>Up}T9_|KJ{-b9mnoS>qI!_q+^?9JD}_Y9c1tt?RSn0pq6+uG)v_=AjgaH~Wd~~%DH_#2KTiS# z7w-{ozTrnh=ZL6^?>G`MuYkaBIbzlZ8yQBCf`I(&6GMi{hKl=|vy7lA!Kah&`OZZ@8)(G!E zOX~E|Fe$WMBKwan2@9pA_lgiZ`u5L33ubhzUd3-72C*9W)R(WKc|`3g$J2Nb5eNPy z7|Yt9G8MGfDWCj{r^PW3gi=ut$(~s_FYxq+0$(_9j*gC6t5x0UUCOoB$L-9i&;DC|heg^CTm-3w}-@6pR9XSKFz>w_^mA}rcs|liS!>350 zN_n97Z2|KGDDI_YWg)uqcxsiF@x}$TLQ?9Bml@hv>srO=<)IVC1*p9 z^qMk!AYZGRCh)?Jy2So6Uo))Y=g->W4?fDNWhzvPI4IPD@n$EG$&H z_Osv|nuPF=LgifcDOJ*=dp3}qj3)zT!(pI(pNI312wW zsP3f5-Nxv?_c07MFU*thCkf!5iYrxXVK$H;c2Fh%XL@{WN$9!(yHB2K*;tlFwMVr1 z@Ndb;0RMg?VkP%&BghylE4YBj&yPD&$#MWw>GM<*zU}(kV+(C-ddo1E?`OWzt4GRg zUDec7Gl_^U@9lXVDR)@ox(7%#`}tWR45DODc(j^6vio5GdqH@6HkWTVUfSmcKR;-X my=;yD{F$m#bA|sQ@xv?NS$5EL(=zo*@R_}`Qb6f-<^KSVBWEK3 literal 0 HcmV?d00001 diff --git a/backend/drm.c b/backend/drm.c index 7a8358c5..7df546d8 100644 --- a/backend/drm.c +++ b/backend/drm.c @@ -34,7 +34,7 @@ typedef struct { /* Linux virtual terminal (VT) */ int vt_fd; - bool vt_active; + volatile sig_atomic_t vt_active; /* written by the VT signal handler */ struct vt_mode old_vtm; /* DRM driver */ @@ -286,32 +286,11 @@ static bool twin_drm_open(twin_drm_t *tx, const char *path) 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) { drmModeDirtyFB(tx->drm_dri_fd, tx->fb_id, NULL, 0); } -static bool twin_drm_update_damage(void *closure) -{ - 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); @@ -342,7 +321,9 @@ static bool twin_drm_work(void *closure) twin_screen_damage(screen, 0, 0, screen->width, screen->height); } - if (!tx->vt_active && twin_screen_damaged(screen)) { + /* No mapping (restoring it failed): keep the damage for the next try */ + if (!tx->vt_active && tx->fb_base != MAP_FAILED && + twin_screen_damaged(screen)) { twin_screen_update(screen); twin_drm_flush(tx); } @@ -457,8 +438,6 @@ twin_context_t *twin_drm_init(int width maybe_unused, int height maybe_unused) /* 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); return ctx; diff --git a/backend/fbdev.c b/backend/fbdev.c index 4f754cf7..2a2f5826 100644 --- a/backend/fbdev.c +++ b/backend/fbdev.c @@ -29,7 +29,7 @@ typedef struct { /* Linux virtual terminal (VT) */ int vt_fd; - bool vt_active; + volatile sig_atomic_t vt_active; /* written by the VT signal handler */ struct vt_mode old_vtm; /* Linux framebuffer */ @@ -174,25 +174,6 @@ static bool twin_fbdev_apply_config(twin_fbdev_t *tx) 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); @@ -223,7 +204,9 @@ static bool twin_fbdev_work(void *closure) twin_screen_damage(screen, 0, 0, screen->width, screen->height); } - if (!tx->vt_active && twin_screen_damaged(screen)) + /* No mapping (restoring it failed): keep the damage for the next try */ + if (!tx->vt_active && tx->fb_base != MAP_FAILED && + twin_screen_damaged(screen)) twin_screen_update(screen); return true; @@ -310,8 +293,6 @@ twin_context_t *twin_fbdev_init(int width maybe_unused, int height maybe_unused) /* 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); return ctx; diff --git a/backend/headless-shm.h b/backend/headless-shm.h index e7d6dc3d..084a27f8 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,6 +33,9 @@ typedef struct { twin_coord_t x, y; twin_count_t button; } pointer; + struct { + twin_ucs4_t ucs4; + } ucs4; } u; } twin_headless_event_t; @@ -72,4 +76,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..c01112ea 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,31 +169,50 @@ 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; } } @@ -203,11 +225,17 @@ static void twin_headless_update_damage(void *closure) if (!twin_screen_damaged(screen)) return; + 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(); @@ -247,7 +275,18 @@ 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 */ + /* One scan-out per iteration, after timeouts, work and input */ + if (twin_screen_damaged(ctx->screen)) + twin_headless_update_damage(ctx); + else { + /* Idle: sleep until the next timeout, but stay responsive to the + * control tool. */ + twin_time_t wait = _twin_timeout_delay(); + if (wait < 0 || wait > 5) + wait = 5; + if (wait > 0) + usleep((useconds_t) wait * 1000); + } /* Update heartbeat timestamp periodically to show backend is alive */ if (tx->shm) { @@ -296,13 +335,17 @@ 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. A + * stale object from a crashed run is removed first; O_EXCL then makes + * sure the backend never adopts one somebody else created. */ + 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)); @@ -350,7 +393,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 +404,7 @@ 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); + /* Updates run once per loop iteration (see poll) so damage coalesces */ return ctx; diff --git a/backend/linux_vt.h b/backend/linux_vt.h index 84a24ddd..309d4243 100644 --- a/backend/linux_vt.h +++ b/backend/linux_vt.h @@ -76,7 +76,7 @@ static inline void twin_vt_setup_signal_handler() static inline bool twin_vt_setup(int *fd_ptr, struct vt_mode *old_vtm, - bool *vt_active) + volatile sig_atomic_t *vt_active) { /* Open VT0 to inquire information */ if ((*fd_ptr = twin_vt_open(0)) < -1) { diff --git a/backend/sdl.c b/backend/sdl.c index 04713fa1..6dfe0fab 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,9 +142,10 @@ 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; @@ -125,11 +158,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); @@ -169,9 +207,20 @@ twin_context_t *twin_sdl_init(int width, int height) static void twin_sdl_configure(twin_context_t *ctx) { + /* Screen pixels are the renderer's logical size, not the (scaled) + * window size; the pixel buffer was allocated for the former. */ int width, height; - SDL_GetWindowSize(PRIV(ctx)->win, &width, &height); - twin_screen_resize(ctx->screen, width, height); + SDL_RenderGetLogicalSize(PRIV(ctx)->render, &width, &height); + if (width > 0 && height > 0) + twin_screen_resize(ctx->screen, width, height); +} + +/* Deliver a character to the focused window */ +static void _twin_sdl_ucs4(twin_screen_t *screen, twin_ucs4_t ucs4) +{ + 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 +228,24 @@ 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: an idle screen should cost no CPU. */ + if (!twin_screen_damaged(screen)) { + twin_time_t wait = _twin_timeout_delay(); + if (wait < 0 || wait > 100) + wait = 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 +263,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 +312,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; } diff --git a/configs/Kconfig b/configs/Kconfig index e44bbc9a..f935da9d 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -225,30 +225,51 @@ config CURSOR Enable custom cursor rendering and manipulation. Allows application control over cursor appearance. -config WINDOW_MANAGER - bool "Enable window manager" +menu "Window Management" + +comment "The 'none' policy is always built: no decorations, no placement" + +config WM_DESKTOP + bool "Desktop window manager" default y help - Enable the window management subsystem: title bars, - decorations, drag-move, click-to-raise, iconify/restore, - and resize handles. + 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. + Selected at run time with twin_screen_set_wm(screen, TwinWmDesktop). - 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. + Costs ~3-4 KB .text on ARM Cortex-M class targets. - Disabling saves ~3-4 KB .text on ARM Cortex-M class targets. +config WM_MOBILE + bool "Mobile window manager" + default y + help + 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. + Selected at run time with twin_screen_set_wm(screen, TwinWmMobile). + +config WM_MOBILE_EVICT + bool "Release pixels of covered applications" + default y + depends on WM_MOBILE + help + Free the pixmap of every application hidden under the top one and + repaint it from application state when it is revealed again. + Keeps memory at one work-area surface regardless of stack depth. config DROP_SHADOW - bool "Render drop shadow for active window" + bool "Window drop shadows" default y - depends on WINDOW_MANAGER + depends on WM_DESKTOP || WM_MOBILE help - Add drop shadow effect to active window. - Provides visual depth and window focus indication. + 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. config HORIZONTAL_OFFSET int "Horizontal offset" @@ -276,20 +297,58 @@ 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 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,10 +406,53 @@ 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, drawn with vector graphics on the mobile window manager. + Settings can switch the running system to the desktop or 'none' + window manager. + +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. + +choice + prompt "Demo scene at startup" + depends on DEMO_APPLICATIONS + default DEMO_SCENE_MOBILE if DEMO_CELLPHONE + default DEMO_SCENE_DESKTOP + help + The demo launcher starts either the phone shell or the desktop + demos. MADO_SCENE=mobile|desktop|none overrides it at run time. + +config DEMO_SCENE_MOBILE + bool "Mobile (cellphone)" + depends on DEMO_CELLPHONE + +config DEMO_SCENE_DESKTOP + bool "Desktop" + depends on WM_DESKTOP + +config DEMO_SCENE_NONE + bool "No window manager" + +endchoice + config DEMO_MULTI bool "Build multi demo" default y - depends on DEMO_APPLICATIONS && WINDOW_MANAGER + depends on DEMO_APPLICATIONS && WM_DESKTOP && WIDGETS help Comprehensive demo with multiple windows and widgets. Demonstrates window management and event handling. @@ -360,7 +462,7 @@ config DEMO_MULTI 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 +471,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 +480,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 +489,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 +499,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..5c03d7ab 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, interact with the phone (the default scene) or, with +`MADO_SCENE=desktop`, move windows and interact with widgets. 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. The backend sleeps +until input or the next timeout when nothing is damaged, and presents once +per screen update. Use Cases: - Desktop applications @@ -393,6 +398,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..ecff7d7a --- /dev/null +++ b/docs/phone.md @@ -0,0 +1,329 @@ +# Mobile and Desktop Scenes + +This document describes how Mado runs a phone user interface and 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 entirely +with anti-aliased vector graphics 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* selected +at run time: + +| Policy | `twin_wm_mode_t` | Behavior | Kconfig | +|--------|------------------|----------|---------| +| none | `TwinWmNone` | 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. | always built | +| desktop | `TwinWmDesktop` | 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 | `TwinWmMobile` | 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` | + +```c +twin_screen_set_wm(screen, TwinWmMobile); /* switch at run time */ +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 cellphone demo's Settings app switches between the +three while running. + +Switching policies re-lays out every existing window: decorations are added +or removed, application windows are cascaded or stacked, and pixels of +windows the new policy keeps hidden are released. A switch requested from +an event handler is applied once that event has been delivered; a later +request replaces it, and if it cannot be queued (out of memory) the call +fails and the current policy stays. `screen->windows` lists +every window bottom to top and is the stacking order for all policies, so a +switch preserves which window is on top. + +The policy interface (`twin_wm_ops_t` in `src/twin_private.h`) has twelve +hooks: enter/leave, layout, map/unmap, decorate, dispatch, hit test, a +pointer filter for gestures, refresh (re-place the stack after a style, +opacity, system-bar, inset or screen-size change), activate (an explicit +`twin_window_activate()`) and work area (what application windows may +cover; a policy without it shows no system bars). 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 always present `none` policy +and the switching logic; `src/wm-desktop.c` and `src/wm-mobile.c` are the +two optional policies. + +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 policy switch) 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.** `twin_pixmap_set_opacity()` applies a constant alpha at + scan-out (fades, dimming during transitions). +- **Procedural shadows.** `twin_pixmap_set_shadow()` attaches 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 and +per path (`twin_path_set_font()`). 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_font_extents()`, `twin_path_set_font()`, +`twin_paint_path_linear()`, the coverage cache controls, tweens and +`twin_now()`. + +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 (Olive, 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. + +Run it with SDL (2x scaled by default; `MADO_SCALE=n` overrides): + +```shell +$ make defconfig && make +$ ./demo-sdl # phone (default scene) +$ MADO_SCENE=desktop ./demo-sdl # desktop demos +$ MADO_SCENE=none ./demo-sdl # single app, 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 four 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 each 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, eviction memory, and run-time switching + including mid-swipe, superseded and out-of-memory requests. +- `test-lifetime`: callbacks that run while an object is torn down, or + that act on the object calling them. +- `test-cellphone`: boots the phone with a fixed clock and drives the whole + user journey through every app. + +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 and policy switches; 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..56c70be5 100644 --- a/include/twin.h +++ b/include/twin.h @@ -19,20 +19,18 @@ * 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 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 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) */ /** * Pixel format enumeration @@ -46,8 +44,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 +60,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,20 +91,6 @@ 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; @@ -141,13 +121,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,10 +155,6 @@ 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 */ @@ -194,176 +168,35 @@ typedef struct _twin_event { } twin_event_t; /** - * Event queue node for event processing - * - * 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 + * Window manager policy * - * 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); + * The window manager is a runtime-switchable policy layered over the + * compositor. Each policy can be compiled out (CONFIG_WM_DESKTOP, + * CONFIG_WM_MOBILE); TwinWmNone is always available: it adds no + * decorations, covers the screen with application windows, and gives the + * keyboard to the newest or the last pressed window. + */ +typedef enum _twin_wm_mode { + TwinWmNone /**< No window management: windows map as requested */, + TwinWmDesktop /**< Overlapping, decorated, draggable windows */, + TwinWmMobile /**< Full-screen app stack with transitions */, +} twin_wm_mode_t; /** - * 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 + * Compositor counters * - * 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 +230,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 +249,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 +258,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 +278,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 +300,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,46 +379,6 @@ 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 @@ -581,8 +387,6 @@ 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; @@ -593,14 +397,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 */ @@ -630,12 +426,6 @@ 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 */, @@ -644,51 +434,7 @@ typedef enum _twin_shape { 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 +442,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 +473,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 @@ -759,15 +486,6 @@ void 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 @@ -847,46 +565,10 @@ 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))) +#define twin_fixed_div(a, b) ((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 @@ -896,24 +578,6 @@ twin_fixed_t twin_fixed_sqrt(twin_fixed_t a); */ 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,15 +587,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 @@ -941,18 +596,6 @@ twin_fixed_t twin_width_ucs4(twin_path_t *path, twin_ucs4_t ucs4); */ 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 +608,12 @@ 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); /* - * 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,15 +626,6 @@ 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, @@ -1008,20 +638,18 @@ void twin_label_set(twin_label_t *label, twin_fixed_t font_size, twin_style_t font_style); -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 label or button is built on, for the twin_widget_* calls */ +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. * @@ -1062,12 +690,8 @@ twin_fixed_t twin_matrix_transform_y(const twin_matrix_t *m, 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 +722,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 +736,36 @@ 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 +773,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 +785,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,6 +793,31 @@ twin_pixmap_t *twin_pixmap_create_const(twin_format_t format, */ void twin_pixmap_destroy(twin_pixmap_t *pixmap); +twin_coord_t twin_pixmap_width(const twin_pixmap_t *pixmap); +twin_coord_t twin_pixmap_height(const twin_pixmap_t *pixmap); +twin_format_t twin_pixmap_format(const twin_pixmap_t *pixmap); + +/** + * 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_show(twin_pixmap_t *pixmap, twin_screen_t *screen, twin_pixmap_t *higher); @@ -1234,25 +828,12 @@ 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); - -void twin_pixmap_set_origin(twin_pixmap_t *pixmap, - twin_coord_t ox, - twin_coord_t oy); - - - void twin_pixmap_clip(twin_pixmap_t *pixmap, twin_coord_t left, twin_coord_t top, 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); @@ -1261,82 +842,24 @@ 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); - +/* 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); -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 @@ -1381,52 +904,32 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, void twin_toplevel_show(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); +/** Outline the widget background is painted with */ +void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape); + +/** Make @widget request the same preferred size as @source */ +void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source); /* Request widget repaint */ void twin_widget_queue_paint(twin_widget_t *widget); -/* 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); - /* * Custom widget support - allows creating widgets without accessing internals * @@ -1435,14 +938,7 @@ twin_widget_t *twin_widget_create_with_handler(twin_box_t *parent, * access to twin_private.h or internal widget structures. */ -/** - * Structure representing a custom widget with user data. - * Encapsulates a base widget and allows attaching custom data. - */ -typedef struct { - twin_widget_t *widget; /**< Base widget functionality */ - void *data; /**< User-defined data */ -} twin_custom_widget_t; +typedef struct _twin_custom_widget twin_custom_widget_t; /** * Create a custom widget with user-defined data and event handler. @@ -1470,14 +966,6 @@ twin_custom_widget_t *twin_custom_widget_create(twin_box_t *parent, twin_widget_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. * @@ -1500,17 +988,6 @@ 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 @@ -1570,6 +1047,31 @@ void twin_window_configure(twin_window_t *window, 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); void twin_window_damage(twin_window_t *window, @@ -1580,6 +1082,293 @@ void twin_window_damage(twin_window_t *window, 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. Return true if the window handled it. + */ +bool 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 + */ + +/** + * Select the window manager policy + * @screen : Screen to manage + * @mode : Policy to activate + * + * Existing windows are re-placed under the new policy: decorations are + * added or removed, app windows are stacked or cascaded, and the pixels + * of windows the policy keeps hidden are released. + * + * Safe to call from an event handler: the switch is then applied once the + * event has been delivered (twin_screen_get_wm() already reports it), and + * a later request replaces it. + * + * Return false if @mode was compiled out, or if the switch could not be + * queued (out of memory); the current policy then stays. If the new + * policy cannot start (out of memory), the screen falls back to TwinWmNone + * and false is returned. + */ +bool twin_screen_set_wm(twin_screen_t *screen, twin_wm_mode_t mode); + +twin_wm_mode_t twin_screen_get_wm(const twin_screen_t *screen); + +/** + * 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; other managers hide them. 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). + */ +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 + */ + +/** + * 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); + +/* + * Text + * + * Text is drawn glyph by glyph from the coverage cache; each glyph is + * rasterized once per (font, size) and composited from then on. + */ + +/** 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); + +/** + * 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); + +/** 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); + +/* + * 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_set_budget(size_t bytes); +void twin_cache_get_stats(twin_cache_stats_t *stats); +void twin_cache_flush(void); + +/* + * Tweens: time-based property animation + */ + +typedef enum _twin_ease { + TwinEaseLinear, + TwinEaseOutCubic, + TwinEaseInOutCubic, + TwinEaseOutBack, +} twin_ease_t; + +typedef struct _twin_tween twin_tween_t; + +/** + * 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); + +/** + * 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); + +/** Stop a tween without calling its step callback again */ +void twin_tween_cancel(twin_tween_t *tween); + +/** Jump a tween to its end state (step callback sees TWIN_FIXED_ONE) */ +void twin_tween_finish(twin_tween_t *tween); + +/** 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); + +/** Monotonic time in milliseconds, the clock used by timeouts and tweens */ +twin_time_t twin_now(void); + #define TWIN_WORK_REDISPLAY 0 #define TWIN_WORK_PAINT 1 #define TWIN_WORK_LAYOUT 2 @@ -1599,11 +1388,6 @@ twin_pixmap_t *twin_tvg_to_pixmap_budget(const char *filepath, twin_format_t fmt, size_t memory_budget); -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); - /** * Memory usage statistics (requires CONFIG_MEMORY_STATS) */ @@ -1619,6 +1403,9 @@ void twin_memory_reset_peak(void); twin_context_t *twin_create(int width, int height); +/** The screen a context drives */ +twin_screen_t *twin_context_screen(const twin_context_t *ctx); + void twin_destroy(twin_context_t *ctx); #endif /* _TWIN_H_ */ diff --git a/mk/common.mk b/mk/common.mk index 57221c52..cb476f90 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -678,8 +678,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-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..9e886b7e --- /dev/null +++ b/src/anim.c @@ -0,0 +1,139 @@ +/* + * 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); + bool last = elapsed >= tween->duration; + twin_fixed_t value; + + if (last) + value = TWIN_FIXED_ONE; + else + value = twin_ease( + tween->ease, + (twin_fixed_t) (((int64_t) elapsed << 16) / tween->duration)); + + tween->running = true; + bool keep = (*tween->proc)(value, tween->closure); + /* twin_tween_finish() from inside proc still promises the end value */ + if (tween->finished && !last && keep) + (*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..dfdbbbc6 100644 --- a/src/api.c +++ b/src/api.c @@ -70,6 +70,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); @@ -112,3 +115,8 @@ void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *)) g_twin_backend.start(ctx, init_callback); } + +twin_screen_t *twin_context_screen(const twin_context_t *ctx) +{ + return ctx->screen; +} diff --git a/src/box.c b/src/box.c index d13c233e..622a2122 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) @@ -165,9 +164,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 +179,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,13 +191,6 @@ 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: box->widget.paint = false; for (child = box->children; child; child = child->next) diff --git a/src/button.c b/src/button.c index d3f29967..70a26a27 100644 --- a/src/button.c +++ b/src/button.c @@ -119,3 +119,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..377e6982 --- /dev/null +++ b/src/cache.c @@ -0,0 +1,177 @@ +/* + * 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, budget; + uint32_t entries, hits, misses, evictions; +} twin_cache_t; + +static twin_cache_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) +{ + 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..eccde1f3 100644 --- a/src/closure.c +++ b/src/closure.c @@ -39,19 +39,11 @@ #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) -{ - 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 _twin_closure_tracker; /* * Find an entry for a given closure pointer. @@ -94,10 +86,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) { @@ -153,10 +141,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 +151,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 +195,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/draw-builtin.c b/src/draw-builtin.c index 423af81f..2641ffac 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -691,6 +691,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; } @@ -756,8 +757,10 @@ 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) { + dy = twin_int_to_fixed(line + xform->off_y); + for (dx = twin_int_to_fixed(xform->off_x); + dx < twin_int_to_fixed(xform->off_x + 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_8(pts[0], pix, sx, sy); @@ -780,8 +783,10 @@ static void twin_pixmap_read_xform_16(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) { + dy = twin_int_to_fixed(line + xform->off_y); + for (dx = twin_int_to_fixed(xform->off_x); + dx < twin_int_to_fixed(xform->off_x + 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); @@ -811,8 +816,10 @@ static void twin_pixmap_read_xform_32(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) { + dy = twin_int_to_fixed(line + xform->off_y); + for (dx = twin_int_to_fixed(xform->off_x); + dx < twin_int_to_fixed(xform->off_x + 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); @@ -889,6 +896,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,6 +913,8 @@ 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; 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..2c30e32d 100644 --- a/src/draw-pixman.c +++ b/src/draw-pixman.c @@ -108,8 +108,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..bb0993f2 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 */ 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..f8302a6a 100644 --- a/src/font.c +++ b/src/font.c @@ -224,7 +224,11 @@ 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 */ + return font->cur_page->offsets[twin_ucs_char_in_page(ucs4)] != 0 || + ucs4 == 0; } #define SNAPX(p) _snap(path, p, snap_x, nsnap_x) @@ -240,9 +244,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 +277,16 @@ 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) +static inline twin_font_t *_twin_path_font(const twin_path_t *path) { - twin_font_t *font = g_twin_font; + 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 +296,16 @@ 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: advance only; vertical metrics come from the font */ + twin_fixed_t adv = FX(b[0], &info); + m->left_side_bearing = 0; + m->right_side_bearing = adv; + 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 +350,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 +364,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 +385,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 +394,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 +403,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 +420,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 +444,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 +458,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 +524,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..4c5e7e57 100644 --- a/src/image-gif.c +++ b/src/image-gif.c @@ -624,7 +624,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 +655,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..cba49973 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -15,7 +15,9 @@ #define D(x) twin_double_to_fixed(x) #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 \ diff --git a/src/label.c b/src/label.c index 59fe926a..0542032b 100644 --- a/src/label.c +++ b/src/label.c @@ -139,3 +139,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/matrix.c b/src/matrix.c index 87e6a994..18f09d99 100644 --- a/src/matrix.c +++ b/src/matrix.c @@ -85,19 +85,6 @@ twin_fixed_t _twin_matrix_determinant(twin_matrix_t *matrix) 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,18 +141,6 @@ 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) diff --git a/src/memstats.c b/src/memstats.c index 726a32ae..4aa66d0d 100644 --- a/src/memstats.c +++ b/src/memstats.c @@ -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; diff --git a/src/path.c b/src/path.c index 8b83da6e..95c45719 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; @@ -203,7 +187,7 @@ 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); @@ -363,7 +347,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 +392,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); @@ -450,7 +434,7 @@ void twin_path_tab(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); @@ -470,16 +454,6 @@ 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 +474,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 +494,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 +544,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 +598,180 @@ 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) +{ + 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 { + /* Reuse the screen's mask buffer, growing it to the high-water mark */ + twin_screen_t *screen = dst->screen; + twin_pixmap_t *mask = NULL; + if (screen && screen->mask_cache && screen->mask_cache_width >= w && + screen->mask_cache_height >= h) { + mask = screen->mask_cache; + } else { + /* Grow to the larger of old and new in each dimension, so a wide + * mask followed by a tall one does not reallocate every time */ + twin_coord_t cw = w, ch = h; + if (screen && screen->mask_cache) { + cw = max(cw, screen->mask_cache_width); + ch = max(ch, screen->mask_cache_height); + } + mask = + twin_pixmap_create(TWIN_A8, screen ? cw : w, screen ? ch : h); + if (!mask) + return false; + if (screen) { + if (screen->mask_cache) + twin_pixmap_destroy(screen->mask_cache); + screen->mask_cache = mask; + screen->mask_cache_width = cw; + screen->mask_cache_height = ch; + } else { + 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); +} + void twin_composite_path(twin_pixmap_t *dst, twin_operand_t *src, twin_coord_t src_x, @@ -664,111 +780,157 @@ 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; + + if (!_twin_path_mask(dst, path, scratch, &m, &mx, &my, &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; +} + +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; + 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; +} - twin_coord_t width = bounds.right - bounds.left; - twin_coord_t height = bounds.bottom - bounds.top; +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); - /* 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; + twin_mask_t m; + twin_coord_t mx, my; + twin_rect_t vis; + if (!_twin_path_mask(dst, path, sp, &m, &mx, &my, &vis)) + return; - if (scratch) { - saved = twin_scratch_save(scratch); - mask = twin_scratch_alloc(scratch, mask_size, _Alignof(twin_pixmap_t)); - } - 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); + 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 */ + for (twin_coord_t y = vis.top; y < vis.bottom; y++) { + twin_fixed_t t = 0; + if (gy) { + twin_fixed_t cy = twin_int_to_fixed(y) + TWIN_FIXED_HALF; + t = (twin_fixed_t) ((((int64_t) cy - y0) * 65536) / gy); + } + twin_operand_t src = {.source_kind = TWIN_SOLID, + .u.argb = _twin_lerp_argb(c0, c1, t)}; + twin_composite(dst, vis.left, y, &src, 0, 0, &msk, vis.left - mx, + y - my, TWIN_OVER, w, 1); } - 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); + _twin_mask_release(&m, sp); + return; } - 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); - - if (mask_from_scratch) { - twin_pixmap_reset_xform_cache(mask); - twin_scratch_restore(scratch, saved); + /* 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; } - /* 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); + 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}; + + for (twin_coord_t y = vis.top; y < vis.bottom; y++) { + if (y == vis.top || gy) { + twin_fixed_t cy = twin_int_to_fixed(y) + TWIN_FIXED_HALF; + for (twin_coord_t i = 0; i < w; i++) { + twin_fixed_t cx = + twin_int_to_fixed(vis.left + i) + TWIN_FIXED_HALF; + int64_t dot = + (((int64_t) cx - x0) * gx + ((int64_t) cy - y0) * gy) >> 16; + twin_fixed_t t = (twin_fixed_t) ((dot * 65536) / len2); + row[i] = _twin_lerp_argb(c0, c1, 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 +948,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 +974,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..96669c80 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,6 +215,68 @@ void twin_pixmap_hide(twin_pixmap_t *pixmap) twin_screen_enable_update(screen); } +void twin_pixmap_hide(twin_pixmap_t *pixmap) +{ + 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); +} + +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) +{ + 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); +} + twin_pointer_t twin_pixmap_pointer(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y) @@ -283,14 +312,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 +386,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 +414,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/screen-ops.c b/src/screen-ops.c index 33c3ef45..54af15e6 100644 --- a/src/screen-ops.c +++ b/src/screen-ops.c @@ -16,27 +16,24 @@ */ static inline twin_argb32_t over(twin_argb32_t dst, twin_argb32_t src) { - uint16_t t1, t2, t3, t4; - twin_a8_t a; + uint32_t a = 0xff - (src >> 24); + if (a == 0) + return src; 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; + return _twin_sat_lanes(_twin_mul_lanes(dst & TWIN_LANES, a) + + (src & TWIN_LANES)) | + _twin_sat_lanes(_twin_mul_lanes((dst >> 8) & TWIN_LANES, a) + + ((src >> 8) & TWIN_LANES)) + << 8; } +/* + * The built-in renderer generates these three span operations along with the + * rest of its compositing table; other renderers (Pixman) get them here. + */ +#if !defined(CONFIG_RENDERER_BUILTIN) /* * Convert RGB16 to ARGB32 and copy to destination * Used for compositing RGB16 pixmaps onto ARGB32 screen buffer @@ -45,7 +42,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; } } @@ -80,3 +79,41 @@ 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 = 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 = 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; +} diff --git a/src/screen.c b/src/screen.c index 10af945f..d6e0daa7 100644 --- a/src/screen.c +++ b/src/screen.c @@ -1,62 +1,92 @@ /* * 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 "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)); 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; + screen->wm = &_twin_wm_none; #ifndef TWIN_SCRATCH_SIZE -#define TWIN_SCRATCH_SIZE (32 * 1024) +#define TWIN_SCRATCH_SIZE (16 * 1024) #endif screen->scratch_buf = twin_malloc(TWIN_SCRATCH_SIZE); screen->scratch_size = screen->scratch_buf ? TWIN_SCRATCH_SIZE : 0; +#if defined(CONFIG_WM_DESKTOP) + twin_screen_set_wm(screen, TwinWmDesktop); +#elif defined(CONFIG_WM_MOBILE) + twin_screen_set_wm(screen, TwinWmMobile); +#endif + /* 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) { + _twin_screen_drop_pending_wm(screen); + + /* Let the policy finish animations and drop pixmaps it owns. */ + if (screen->wm->leave) + screen->wm->leave(screen); + screen->wm = &_twin_wm_none; + + while (screen->windows) + twin_window_destroy(screen->windows); while (screen->bottom) twin_pixmap_hide(screen->bottom); + if (screen->background) + twin_pixmap_destroy(screen->background); for (int i = 0; i < screen->path_cache_count; i++) twin_path_destroy(screen->path_cache[i]); if (screen->mask_cache) @@ -66,23 +96,9 @@ void twin_screen_destroy(twin_screen_t *screen) 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,552 @@ 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 - /* bounds check in y */ - if (y < p->y) - return; - if (p->y + p->height <= y) - return; +typedef struct { + twin_coord_t l, r; +} twin_span_t; - /* 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) +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; - 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); + /* Merge with every interval that touches [l, r). */ + int i = 0, o = 0; + twin_span_t out[TWIN_SPAN_SET_MAX + 1]; + bool placed = false; + for (; i < set->n; i++) { + twin_span_t s = set->s[i]; + if (s.r < l) { + out[o++] = s; + } else if (r < s.l) { + if (!placed) { + out[o++] = (twin_span_t) {l, r}; + placed = true; + } + out[o++] = s; + } else { + l = min(l, s.l); + r = max(r, s.r); + } + } + if (!placed) + out[o++] = (twin_span_t) {l, r}; + if (o > TWIN_SPAN_SET_MAX) + return; /* keep the old (smaller) set: conservative */ + memcpy(set->s, out, sizeof(out[0]) * o); + set->n = o; } -void twin_screen_update(twin_screen_t *screen) +/* 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) { - 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; + 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; +} - pop16 = _twin_rgb16_source_argb32; - pop32 = _twin_argb32_over_argb32; - bop32 = _twin_argb32_source_argb32; +/* + * 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]; +static int shadow_ramp_blur = -1; - if (right > screen->width) - right = screen->width; - if (bottom > screen->height) - bottom = screen->height; +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 = ((int64_t) i << 16) / (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; +} - 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; +/* 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; +} - screen->damage.left = screen->damage.right = 0; - screen->damage.top = screen->damage.bottom = 0; +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; +} - /* Reuse cached span buffer if large enough */ - if (screen->span_cache && screen->span_cache_width >= width) { - span = screen->span_cache; +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 (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); + } + return r; +} + +/* 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); + uint32_t alpha = a + (ag >> 16); + return rb | (ag & 0xff) << 8 | (alpha > 255 ? 255 : alpha) << 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; + + /* 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; + + twin_argb32_t *d = span + (l - span_left); + 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++; + } +} + +/* + * 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 + +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; + +static inline bool _layer_hidden(const twin_pixmap_t *p) +{ + return !p->opacity || _twin_pixmap_is_stub(p); +} + +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; + + 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) + _twin_argb32_source_argb32((twin_pointer_t) {.argb32 = dst}, s, w); + 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++) { + uint32_t a = (uint32_t) src.a8[i] * p->opacity / 255; + 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); + } +} + +/* Is @p, on row @y, opaque across [l, r)? 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; + 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; +} + +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 (!p->shadow.blur || !p->shadow.alpha) + 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 (p->shadow.blur && p->shadow.alpha) { + 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; + } + + /* 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 { + _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 (a policy switch waits) */ + _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 +738,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) { @@ -351,10 +797,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 ask for a policy switch; it waits until 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 +900,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 (screen->wm->filter && screen->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..9e0a1a27 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; } @@ -202,7 +230,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..a635daaf 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -60,8 +60,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, @@ -95,7 +109,7 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, 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; @@ -107,8 +121,9 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, 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 +137,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,7 +167,8 @@ 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); } } @@ -160,3 +179,8 @@ void twin_toplevel_show(twin_toplevel_t *toplevel) _twin_toplevel_paint(toplevel); twin_window_show(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..0cb8a31d 100644 --- a/src/trig.c +++ b/src/trig.c @@ -7,15 +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; @@ -23,22 +14,6 @@ twin_fixed_t twin_cos(twin_angle_t a) 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 +134,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..37ee2430 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -16,6 +16,442 @@ #include "twin.h" +#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; + +/** + * 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 */ + twin_shadow_t shadow; /**< Procedural drop shadow */ + 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 */ + const struct _twin_wm_ops *wm; /**< Active policy */ + void *wm_data; /**< Policy private state */ + twin_window_t *windows; /**< Every window, bottom to top */ + twin_window_t *bar_top; /**< System bar above the work area */ + twin_window_t *bar_bottom; /**< System bar below the work area */ + const struct _twin_wm_ops *wm_pending; /**< Switch requested mid-event */ + struct _twin_work *wm_work; /**< Work item applying it */ + int dispatching; /**< Busy depth: dispatch, relayout, switch */ + twin_coord_t inset_bottom; /**< Reserved above the bottom bar (IME) */ + twin_window_t *zombies; /**< Destroyed while busy, freed when idle */ + + 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; + + /* 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); +}; + +#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 */ + twin_window_style_t style; /**< Window style */ + twin_format_t format; /**< Pixel format 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; /**< Active window flag */ + bool client_grab; /**< Mouse grab state */ + bool draw_queued; /**< Draw pending flag */ + bool opaque; /**< Client area is fully painted by draw() */ + bool mapped; /**< Shown by the application */ + bool frame_dirty; /**< Decorations need repainting */ + bool dead; /**< Destroyed; the struct outlives a busy screen */ + bool wm_placed; /**< Policy already chose a position (desktop) */ + bool wm_maximized; /**< Covers the work area; wm_saved is the request */ + twin_rect_t wm_saved; /**< Request to restore after maximize */ + + 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 */ + 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 */ +}; + +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 */ + 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 */ +}; + +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; /**< Base widget functionality */ + void *data; /**< User-defined data */ +}; + +/* + * 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 +482,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,21 +588,17 @@ 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_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) +#define twin_fixed_to_xfixed(f) (((twin_xfixed_t) (f)) * 65536) /* * twin_sfixed_t a = b'10100; @@ -200,8 +627,6 @@ typedef int64_t twin_xfixed_t; #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,7 +732,6 @@ 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) @@ -321,10 +745,29 @@ 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; +} + #define twin_in(s, i, m, t) \ ((twin_argb32_t) twin_int_mult(twin_get_8(s, i), (m), (t)) << (i)) @@ -336,14 +779,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)) @@ -400,6 +835,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 +885,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 +896,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 @@ -496,12 +918,6 @@ twin_sfixed_t _twin_matrix_dy(twin_matrix_t *m, 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 */ @@ -713,26 +1129,375 @@ typedef struct twin_backend { } twin_backend_t; /* - * Visual effect stuff + * Window manager policy interface + * + * A policy decides window geometry, decorations, stacking, and how pointer + * events reach clients. window.c owns the window objects and calls into the + * active policy at a handful of well-defined points; policies never free a + * window. The 'none' policy (wm.c) is always present. + */ +typedef struct _twin_wm_ops { + twin_wm_mode_t mode; + + /* Policy becomes active / inactive. enter() re-lays out and maps every + * window; returning false (e.g. out of memory for its state) makes the + * screen fall back to the 'none' policy. */ + bool (*enter)(twin_screen_t *screen); + void (*leave)(twin_screen_t *screen); + + /* Compute pixmap geometry (screen coordinates) and decoration border */ + void (*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 (*map)(twin_window_t *window); + void (*unmap)(twin_window_t *window, bool destroying); + + /* Paint decorations into window->pixmap (NULL: none) */ + void (*decorate)(twin_window_t *window); + + /* Deliver an event targeted at a window (NULL: forward to client) */ + bool (*dispatch)(twin_window_t *window, twin_event_t *event); + + /* Pointer hit test in screen coordinates (NULL: pixmap rectangle) */ + bool (*hit)(twin_window_t *window, twin_coord_t x, twin_coord_t y); + + /* 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 (*filter)(twin_screen_t *screen, 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 (NULL: nothing + * to do). Every window has been re-laid out already. */ + void (*refresh)(twin_screen_t *screen); + + /* Explicit activation request (twin_window_activate); NULL: map() */ + void (*activate)(twin_window_t *window); + + /* Shrink @area (preset to the whole screen) to what application windows + * may cover, e.g. minus the system bars. NULL: the whole screen, and + * the policy does not show system bars, so twin_screen_set_bars() hides + * them. */ + void (*work_area)(const twin_screen_t *screen, twin_rect_t *area); +} twin_wm_ops_t; + +extern const twin_wm_ops_t _twin_wm_none; +#if defined(CONFIG_WM_DESKTOP) +extern const twin_wm_ops_t _twin_wm_desktop; +#endif +#if defined(CONFIG_WM_MOBILE) +extern const twin_wm_ops_t _twin_wm_mobile; +#endif + +/* window.c services used by policies */ +void _twin_window_relayout(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); +bool _twin_window_is_bar(const twin_window_t *window); +/* 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 + * policy switch). 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: forget a policy switch queued from an event handler */ +void _twin_screen_drop_pending_wm(twin_screen_t *screen); + +/* 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); -#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 { + struct _twin_cache_entry *hnext; /* hash chain */ + struct _twin_cache_entry *prev, *next; /* LRU list */ + uint64_t key; + 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); + +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); + +/** + * 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 */ #ifdef _MSC_VER @@ -819,13 +1584,6 @@ 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, @@ -851,35 +1609,19 @@ 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); 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); @@ -931,7 +1673,7 @@ static inline bool twin_pointer_valid(const void *ptr) */ /* Maximum number of tracked closures */ -#define TWIN_MAX_CLOSURES 1024 +#define TWIN_MAX_CLOSURES 128 /* Closure tracking entry */ typedef struct _twin_closure_entry { @@ -944,16 +1686,12 @@ typedef struct _twin_closure_entry { 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_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 +1778,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 +1802,4 @@ 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; -} - #endif /* _TWIN_PRIVATE_H_ */ diff --git a/src/widget.c b/src/widget.c index e1922844..cb10180f 100644 --- a/src/widget.c +++ b/src/widget.c @@ -101,6 +101,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; @@ -213,46 +217,30 @@ void _twin_widget_bevel(twin_widget_t *widget, twin_fixed_t b, bool down) } } -void twin_widget_children_paint(twin_box_t *box) +void twin_widget_set(twin_widget_t *widget, twin_argb32_t background) { - for (twin_widget_t *child = box->children; child; child = child->next) - _twin_widget_queue_paint(child); + widget->background = background; + _twin_widget_queue_paint(widget); } -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) +void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape) { - 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; + widget->shape = shape; + _twin_widget_queue_paint(widget); } -void twin_widget_set(twin_widget_t *widget, twin_argb32_t background) +void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source) { - widget->background = background; - _twin_widget_queue_paint(widget); + widget->copy_geom = source; + _twin_widget_queue_layout(widget); } -twin_fixed_t twin_widget_width(twin_widget_t *widget) +static twin_fixed_t twin_widget_width(twin_widget_t *widget) { return _twin_widget_width(widget); } -twin_fixed_t twin_widget_height(twin_widget_t *widget) +static twin_fixed_t twin_widget_height(twin_widget_t *widget) { return _twin_widget_height(widget); } @@ -262,13 +250,14 @@ void twin_widget_queue_paint(twin_widget_t *widget) _twin_widget_queue_paint(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_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) { twin_widget_t *widget = twin_malloc(sizeof(twin_widget_t)); if (!widget) @@ -356,7 +345,13 @@ static twin_dispatch_result_t custom_widget_dispatch(twin_widget_t *widget, entry = entry->next; } - /* Unregister from custom widget map before base destruction */ + /* The toolkit owns the wrapper and its data once the widget is + * created; release them with the widget. */ + twin_custom_widget_t *custom = find_custom_widget(widget); + if (custom) { + twin_free(custom->data); + twin_free(custom); + } unregister_custom_widget(widget); /* Now call base widget dispatch to complete destruction */ @@ -420,23 +415,7 @@ twin_custom_widget_t *twin_custom_widget_create(twin_box_t *parent, 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; @@ -453,11 +432,6 @@ 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) -{ - return custom ? custom->widget : NULL; -} - twin_fixed_t twin_custom_widget_width(twin_custom_widget_t *custom) { return custom ? twin_widget_width(custom->widget) : 0; @@ -491,22 +465,3 @@ void twin_widget_set_callback(twin_widget_t *widget, 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; -} diff --git a/src/window.c b/src/window.c index 69c9bac3..4e506769 100644 --- a/src/window.c +++ b/src/window.c @@ -1,30 +1,108 @@ /* * 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; + } + } +} + +bool _twin_window_is_bar(const twin_window_t *window) +{ + return window == window->screen->bar_top || + window == window->screen->bar_bottom; +} + +bool _twin_window_shown(const twin_window_t *window) +{ + return window->pixmap && window->pixmap->screen && + !_twin_pixmap_is_stub(window->pixmap); +} + +bool _twin_window_managed(const twin_window_t *window) +{ + return window->mapped && window->style != TwinWindowPlain && + !_twin_window_is_bar(window); +} + +/* 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}; +} -#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) +/* 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; +} -/* Shadow color for CSS-style effect: semi-transparent black (50% opacity). */ -#define SHADOW_COLOR 0x80000000 +static void _twin_window_reset_clip(twin_window_t *window) +{ + _twin_window_clip(window, &window->client); +} -#endif /* CONFIG_WINDOW_MANAGER */ +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,253 +112,272 @@ 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->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 + window->format = format; + window->request = (twin_rect_t) {x, x + width, y, y + height}; + window->opaque = format == TWIN_RGB16; + + screen->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 (screen->bar_top == window) + screen->bar_top = NULL; + if (screen->bar_bottom == window) + screen->bar_bottom = NULL; + + /* 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). + */ + window->mapped = false; + screen->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); + } twin_free(window->name); - twin_free(window); + window->name = NULL; + window->pixmap = NULL; + window->event = NULL; + window->draw = NULL; + window->destroy = NULL; + window->dead = true; + + if (was_bar) + _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; + window->screen->wm->map(window); } void twin_window_hide(twin_window_t *window) { - twin_pixmap_hide(window->pixmap); + if (!window->mapped) + return; + window->mapped = false; + window->screen->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; - -#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; -#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); + twin_pixmap_t *old = window->pixmap; + twin_screen_t *screen = window->screen; + + fresh->window = window; + fresh->opacity = old->opacity; + fresh->shadow = old->shadow; + 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 */ + if (!pixmap) + return; + window->screen->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}; + window->wm_maximized = false; + /* 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->wm->refresh) + /* The style may move the window between policy layers */ + screen->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); + twin_window_draw(window); + return true; +} void twin_window_set_name(twin_window_t *window, const char *name) { @@ -292,243 +389,58 @@ 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); + if (window->screen->wm->decorate) { + window->frame_dirty = true; + twin_window_queue_paint(window); + } } -#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->wm->refresh) + window->screen->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 (window->screen->wm->decorate) { + window->screen->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 +458,12 @@ 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}); } static bool _twin_window_repaint(void *closure) @@ -569,6 +471,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 +481,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 (window->screen->wm->hit) + return window->screen->wm->hit(window, x, y); + twin_pixmap_t *p = window->pixmap; + return p->x <= x && x < p->x + p->width && p->y <= y && + y < p->y + p->scan_height; +} -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) +{ + if (window->screen->wm->dispatch) + return window->screen->wm->dispatch(window, event); + return _twin_window_deliver(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..237849ee --- /dev/null +++ b/src/wm-desktop.c @@ -0,0 +1,748 @@ +/* + * 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 *) (screen)->wm_data) + +static void desktop_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); +} + +static void desktop_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); +} + +static void desktop_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 */ +static void desktop_activate(twin_window_t *window) +{ + _twin_pixmap_set_scan_height(window->pixmap, window->pixmap->height); + desktop_map(window); +} + +static void desktop_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; + } +} + +static bool desktop_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) + desktop_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; +} + +static bool desktop_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; + } + desktop_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; + desktop_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); +} + +static bool desktop_enter(twin_screen_t *screen) +{ + twin_desktop_t *d = twin_calloc(1, sizeof(*d)); + if (!d) + return false; + screen->wm_data = d; + + /* Place every application window afresh, bottom to top (leave cleared + * wm_placed) */ + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + _place_new(w); + _twin_window_relayout(w); + } + /* Map in stacking order; desktop_map raises, so iterate a snapshot. */ + twin_window_t *top = NULL; + int n = 0; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + n++; + for (int i = 0; i < n; i++) { + twin_window_t *w = screen->windows; + /* Activation handlers may have destroyed windows meanwhile */ + if (!w) + break; + if (w->mapped && !_twin_window_is_bar(w)) { + desktop_map(w); + if (w->style != TwinWindowPlain) + top = w; + } else { + _twin_window_raise(w); + } + } + if (top) + _activate(top); + return true; +} + +/* The screen or the work area changed size: refit maximized windows and + * bring everything else back on screen */ +static void desktop_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 */ + } +} + +static void desktop_leave(twin_screen_t *screen) +{ + twin_desktop_t *d = DESKTOP(screen); + if (d->resize_work) + twin_clear_work(d->resize_work); + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + w->active = false; + w->client_grab = false; + /* Placement is redone on the next entry; maximized stays as is */ + w->wm_placed = false; + /* A window being destroyed may already have lost its pixmap */ + if (w->pixmap) + twin_pixmap_set_shadow(w->pixmap, 0, 0, 0, 0); + } + twin_free(screen->wm_data); + screen->wm_data = NULL; +} + +const twin_wm_ops_t _twin_wm_desktop = { + .mode = TwinWmDesktop, + .enter = desktop_enter, + .leave = desktop_leave, + .layout = desktop_layout, + .map = desktop_map, + .unmap = desktop_unmap, + .decorate = desktop_decorate, + .dispatch = desktop_dispatch, + .hit = desktop_hit, + .refresh = desktop_refresh, + .activate = desktop_activate, +}; diff --git a/src/wm-mobile.c b/src/wm-mobile.c new file mode 100644 index 00000000..d1290bad --- /dev/null +++ b/src/wm-mobile.c @@ -0,0 +1,628 @@ +/* + * 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 *) (screen)->wm_data) + +static bool _animate(void) +{ + return !(_twin_debug & TWIN_DEBUG_NO_ANIMATION); +} + +static void mobile_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; + mobile_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 +} + +static void mobile_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); +} + +static void mobile_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 */ +static bool mobile_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 mobile_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 */ +static bool mobile_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; +} + +static bool mobile_enter(twin_screen_t *screen) +{ + twin_mobile_t *m = twin_calloc(1, sizeof(*m)); + if (!m) + return false; + screen->wm_data = m; + screen->background_color = 0xff000000; + for (twin_window_t *w = screen->windows; w; w = w->wm_next) + _twin_window_relayout(w); + mobile_settle(screen); + return true; +} + +/* A mapped window changed style or opacity: rebuild the arrangement */ +static void mobile_refresh(twin_screen_t *screen) +{ + _finish(screen); + mobile_settle(screen); +} + +static void mobile_leave(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); + twin_free(m); + screen->wm_data = NULL; +} + +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) */ +static void mobile_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); +} + +const twin_wm_ops_t _twin_wm_mobile = { + .mode = TwinWmMobile, + .enter = mobile_enter, + .leave = mobile_leave, + .layout = mobile_layout, + .map = mobile_map, + .unmap = mobile_unmap, + .dispatch = mobile_dispatch, + .filter = mobile_filter, + .refresh = mobile_refresh, + .work_area = mobile_work_area, +}; diff --git a/src/wm.c b/src/wm.c new file mode 100644 index 00000000..f9aec481 --- /dev/null +++ b/src/wm.c @@ -0,0 +1,306 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Window manager selection and the 'none' policy + * + * The 'none' policy is the floor every build has: 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" + +static void none_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}; +} + +static void none_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); + 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); +} + +static void none_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); +} + +static bool none_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); +} + +static bool none_enter(twin_screen_t *screen) +{ + for (twin_window_t *w = screen->windows; w; w = w->wm_next) { + _twin_window_relayout(w); + if (w->mapped) + none_map(w); + } + return true; +} + +const twin_wm_ops_t _twin_wm_none = { + .mode = TwinWmNone, + .enter = none_enter, + .layout = none_layout, + .map = none_map, + .unmap = none_unmap, + .dispatch = none_dispatch, +}; + +static const twin_wm_ops_t *_twin_wm_lookup(twin_wm_mode_t mode) +{ + switch (mode) { + case TwinWmNone: + return &_twin_wm_none; +#if defined(CONFIG_WM_DESKTOP) + case TwinWmDesktop: + return &_twin_wm_desktop; +#endif +#if defined(CONFIG_WM_MOBILE) + case TwinWmMobile: + return &_twin_wm_mobile; +#endif + default: + return NULL; + } +} + +static void _twin_screen_switch_wm(twin_screen_t *screen, + const twin_wm_ops_t *ops) +{ + if (screen->wm == ops) + return; + + _twin_screen_busy(screen); + twin_screen_disable_update(screen); + if (screen->wm->leave) + screen->wm->leave(screen); + + /* Neutral state: nothing shown, no policy-specific attributes. */ + 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; + twin_pixmap_hide(w->pixmap); + w->pixmap->scan_height = w->pixmap->height; /* not iconified */ + w->active = false; + w->pixmap->opacity = 0xff; + w->pixmap->shadow = (twin_shadow_t) {0}; + } + screen->background_color = 0xffffffff; + screen->active = NULL; + screen->target = NULL; + screen->clicklock = false; + + screen->wm = ops; + if (!ops->enter || !ops->enter(screen)) { + screen->wm = &_twin_wm_none; + none_enter(screen); + } + twin_screen_damage(screen, 0, 0, screen->width, screen->height); + twin_screen_enable_update(screen); + _twin_screen_idle(screen); +} + +void _twin_screen_drop_pending_wm(twin_screen_t *screen) +{ + if (screen->wm_work) + twin_clear_work(screen->wm_work); + screen->wm_work = NULL; + screen->wm_pending = NULL; +} + +static bool _twin_screen_apply_pending_wm(void *closure) +{ + twin_screen_t *screen = closure; + const twin_wm_ops_t *ops = screen->wm_pending; + screen->wm_pending = NULL; + screen->wm_work = NULL; + if (ops) + _twin_screen_switch_wm(screen, ops); + return false; +} + +bool twin_screen_set_wm(twin_screen_t *screen, twin_wm_mode_t mode) +{ + const twin_wm_ops_t *ops = _twin_wm_lookup(mode); + if (!ops) + return false; + + /* Switching re-lays out every window, including the one whose event + * is being delivered, and frees the old policy's state: never do that + * underneath a running dispatch. */ + if (screen->dispatching) { + if (!screen->wm_work) + screen->wm_work = twin_set_work(_twin_screen_apply_pending_wm, + TWIN_WORK_LAYOUT, screen); + /* Out of memory: refuse rather than switch under the dispatch */ + if (!screen->wm_work) + return false; + screen->wm_pending = ops == screen->wm ? NULL : ops; + return true; + } + /* A newer request replaces whatever a handler queued */ + _twin_screen_drop_pending_wm(screen); + _twin_screen_switch_wm(screen, ops); + return screen->wm == ops; +} + +twin_wm_mode_t twin_screen_get_wm(const twin_screen_t *screen) +{ + return screen->wm_pending ? screen->wm_pending->mode : screen->wm->mode; +} + +void _twin_screen_relayout(twin_screen_t *screen) +{ + /* 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); + /* Bars belong to policies that carve a work area out for them */ + if (!screen->wm->work_area) { + if (screen->bar_top && screen->bar_top->pixmap) + twin_pixmap_hide(screen->bar_top->pixmap); + if (screen->bar_bottom && screen->bar_bottom->pixmap) + twin_pixmap_hide(screen->bar_bottom->pixmap); + } + if (screen->wm->refresh) + screen->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) +{ + /* One window cannot be both bars, and bars live on this screen */ + if ((top && top->screen != screen) || (bottom && bottom->screen != screen)) + return; + 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); +} + +void twin_screen_set_inset(twin_screen_t *screen, twin_coord_t bottom) +{ + if (bottom < 0) + bottom = 0; + if (bottom == screen->inset_bottom) + return; + screen->inset_bottom = bottom; + _twin_screen_relayout(screen); +} + +twin_rect_t twin_screen_work_area(const twin_screen_t *screen) +{ + twin_rect_t area = {0, screen->width, 0, screen->height}; + if (screen->wm->work_area) + screen->wm->work_area(screen, &area); + area.bottom -= screen->inset_bottom; + if (area.bottom < area.top) + area.bottom = area.top; + return area; +} + +void twin_window_activate(twin_window_t *window) +{ + const twin_wm_ops_t *wm = window->screen->wm; + if (!window->mapped || _twin_window_is_bar(window)) + return; + /* Every policy raises and focuses a window it maps */ + if (wm->activate) + wm->activate(window); + else + wm->map(window); +} + +bool 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); + return handled; +} + +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..f4ba5cd8 --- /dev/null +++ b/tests/golden/cellphone.txt @@ -0,0 +1,63 @@ +# Golden frame checksums for the cellphone suite. +# Regenerate with: make check-update +# config 2336132b +lock 052098fa +home 1a3dc03b +calculator 749994d4 +dialer-empty 7e87b318 +dialer-number 13143cec +dialer-long 682aefe0 +dialer-incall dfa59b1d +dialer-incall-unknown bd5afaa5 +calllog 341b4359 +calllog-end 94910b17 +calllog-call 8364eedc +contacts-list 27183210 +contacts-list-scrolled 4d381d80 +contacts-detail 72a4ba61 +contacts-form-keyboard 71222ee3 +contacts-list-added b0df3666 +contacts-form-validation 9a5acb98 +contacts-form-full 7ed67600 +sms-list 4c373d97 +sms-chat d1c6c574 +sms-typing c02c14bd +sms-sent d806a778 +sms-chat-dark 6e0f9a2c +music-list cf42b3c5 +music-now-playing 19eb07ff +music-paused bd052fd3 +music-seeked b16ad7d6 +music-repeat ea55957a +music-shuffle ee978eb8 +music-now-playing-dark 5f0607dd +music-list-playing fdee372f +music-list-scrolled 28a25944 +music-list-reopened 4be6db76 +photos-grid 939d998d +photos-viewer 4825f7f3 +photos-viewer-first 819e1081 +photos-viewer-bare 218e0f56 +camera d68c4510 +camera-shot 6e9386eb +camera-video 63db382b +camera-selfie 946f8ecd +settings a066cd81 +settings-display 58065c6c +settings-sound 6b5df6cf +settings-wallpaper 7a91c018 +settings-time 5b4ffa59 +settings-display-dark 85a1f340 +settings-home-dark e14d1f87 +settings-sound-changed 54b18173 +settings-wallpaper-dusk d9c420f3 +settings-wm 9ed9fd7e +settings-desktop 32891299 +settings-wm-mobile 9ed9fd7e +settings-none f60f298b +games-paused 01480e92 +games-snake 7d15230d +games-pong d2b0caba +games-tetris 31fd1cac +games-tetris-late 4f253509 +games-tetris-revealed 29ee7bdc diff --git a/tests/golden/core.txt b/tests/golden/core.txt new file mode 100644 index 00000000..b173571e --- /dev/null +++ b/tests/golden/core.txt @@ -0,0 +1,4 @@ +# Golden frame checksums for the core suite. +# Regenerate with: make check-update +# config 2336132b +scene 373a3045 diff --git a/tests/harness.c b/tests/harness.c new file mode 100644 index 00000000..d7d0dd73 --- /dev/null +++ b/tests/harness.c @@ -0,0 +1,476 @@ +/* + * 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, twin_wm_mode_t wm) +{ + 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) { + free(t->fb); + free(t); + return NULL; + } + twin_screen_set_wm(t->screen, wm); + 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; spent += 16) { + th_frame(t); + twin_time_t delay = _twin_timeout_delay(); + if (delay < 0 || delay > max_ms - spent) + break; + virtual_now += 16; + } + 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); +} + +static 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]; +} + +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); + } + if (t->screen->mask_cache) + retained += + sizeof(twin_pixmap_t) + (size_t) t->screen->mask_cache->stride * + t->screen->mask_cache->height; + 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 " + "wm=%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 + /* Screens list the policies that are built (Settings) */ + 0 +#if defined(CONFIG_WM_DESKTOP) + | 1 +#endif +#if defined(CONFIG_WM_MOBILE) + | 2 +#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"; +} + +static bool golden_lookup(const char *suite, + const char *name, + uint32_t *crc, + bool *config_ok) +{ + char path[512], line[256]; + snprintf(path, sizeof(path), "%s/%s.txt", golden_dir(), suite); + FILE *f = fopen(path, "r"); + *config_ok = false; + 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_ok = v == config_fingerprint(); + 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; + bool config_ok; + bool found = golden_lookup(suite, name, &want, &config_ok); + if (!config_ok) { + golden_skipped++; + 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..a4381340 --- /dev/null +++ b/tests/harness.h @@ -0,0 +1,123 @@ +/* + * 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 given window manager */ +th_t *th_create(twin_coord_t width, twin_coord_t height, twin_wm_mode_t wm); +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); + +/* + * 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) */ +#define CHECK_EQ(a, b, what) \ + do { \ + long long _a = (long long) (a), _b = (long long) (b); \ + CHECK(_a == _b, "%s: got %lld, expected %lld", what, _a, _b); \ + } while (0) + +/* NUL-terminated strings; @a may be NULL */ +#define CHECK_STR(a, b, what) \ + do { \ + const char *_s = (a), *_t = (b); \ + CHECK(_s && !strcmp(_s, _t), "%s: got \"%s\", expected \"%s\"", what, \ + _s ? _s : "(null)", _t); \ + } 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..2b8651e8 --- /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(PHONE_W - 20, PHONE_HEADER_H / 2); + 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-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..fe1e51db --- /dev/null +++ b/tests/phone-music.c @@ -0,0 +1,189 @@ +/* + * 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"); + int track = app_music_test_track(); + 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(track != last || app_music_test_playing(), "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..a4b06ffa --- /dev/null +++ b/tests/phone-scenes.c @@ -0,0 +1,94 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Every app under every scene: the same app code must fill whatever client + * area the 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; +} + +/* Switch the scene under the open app @v and check it re-laid out */ +static void check_scene(ui_view_t *v, + const char *name, + twin_wm_mode_t mode, + const char *scene, + twin_coord_t want_h) +{ + CHECK(phone_set_scene(mode), "%s under %s", name, scene); + ph_wait(500); + char what[64]; + snprintf(what, sizeof(what), "%s: %s view height", scene, name); + if (want_h) + CHECK_EQ(ui_view_height(v), want_h, what); + int n = unpainted(v); + CHECK(n == 0, "%s: %s left %d client pixels unpainted", scene, name, n); +} + +/* + * Open each app once and cycle the scenes underneath it. Every switch + * resizes the view, so this covers both laying out at creation (mobile) + * and re-laying out in place for each other scene. + */ +void test_scenes(void) +{ + for (int i = 0; i < phone_app_count(); i++) { + 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_scene(v, name, TwinWmNone, "none", PHONE_H); + check_scene(v, name, TwinWmMobile, "mobile after none", + PHONE_CONTENT_H); +#if defined(CONFIG_WM_DESKTOP) + check_scene(v, name, TwinWmDesktop, "desktop", 0); + check_scene(v, name, TwinWmMobile, "mobile after desktop", + PHONE_CONTENT_H); +#endif + ph_check(name); + ph_home(); + } +} diff --git a/tests/phone-settings.c b/tests/phone-settings.c new file mode 100644 index 00000000..fe70064c --- /dev/null +++ b/tests/phone-settings.c @@ -0,0 +1,294 @@ +/* + * 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, the window manager scene switches to + * Desktop and None and back, 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("Olive"); + 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_window_manager(void) +{ + twin_screen_t *screen = phone_screen(); + if (!open_sub("Window manager")) + return; + ph_shot("settings-wm"); + +#if defined(CONFIG_WM_DESKTOP) + /* Desktop: views get title bars, the bars disappear */ + tap_item("Desktop"); + CHECK_EQ(twin_screen_get_wm(screen), TwinWmDesktop, "desktop scene"); + CHECK(top_is("Window manager"), "settings still on top in desktop"); + ph_shot("settings-desktop"); + tap_item("Mobile"); + CHECK_EQ(twin_screen_get_wm(screen), TwinWmMobile, "back to mobile"); + CHECK_EQ(phone_depth(), 2, "stack intact after desktop"); + CHECK_EQ(ui_view_height(phone_top()), PHONE_CONTENT_H, + "work-area height restored"); + ph_shot("settings-wm-mobile"); +#else + ui_rect_t none; + CHECK(!app_settings_item_rect("Desktop", &none), + "compiled-out desktop policy not offered"); +#endif + + /* None: every view covers the screen; the header offers Back */ + tap_item("None"); + CHECK_EQ(twin_screen_get_wm(screen), TwinWmNone, "none scene"); + CHECK_EQ(ui_view_height(phone_top()), PHONE_H, "view resized to screen"); + ph_shot("settings-none"); + view_tap(14, PHONE_HEADER_H / 2); + CHECK(top_is("Settings"), "header Back works without a nav bar"); + open_sub("Window manager"); + tap_item("Mobile"); + CHECK_EQ(twin_screen_get_wm(screen), TwinWmMobile, "mobile again"); + CHECK_EQ(ui_view_height(phone_top()), PHONE_CONTENT_H, + "view back in the work area"); + + /* The phone still navigates normally */ + back_to_root(); + if (open_sub("Display")) + back_to_root(); + CHECK_EQ(th_damage_check(phone_th), 0, "consistent after scene switches"); +} + +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_window_manager(); + test_about(); + + ph_home(); + CHECK_EQ(phone_theme_index(), 0, "theme 0 at exit"); + CHECK_EQ(phone_wallpaper(), 0, "wallpaper 0 at exit"); + CHECK_EQ(twin_screen_get_wm(phone_screen()), TwinWmMobile, + "mobile scene 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..35ad5158 --- /dev/null +++ b/tests/phone-test.h @@ -0,0 +1,65 @@ +/* + * 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_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..fc3afd6a --- /dev/null +++ b/tests/test-cellphone.c @@ -0,0 +1,272 @@ +/* + * 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, window-manager scene switch). + * + * 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 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++) { + 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++) { + 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, TwinWmMobile); + 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_regress}; + for (size_t i = 0; i < sizeof(suites) / sizeof(suites[0]); i++) { + suites[i](); + if (phone_depth()) + ph_home(); + } + test_budgets(); + + phone_shutdown(); + 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..d61a9b8a --- /dev/null +++ b/tests/test-core.c @@ -0,0 +1,997 @@ +/* + * 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 "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, TwinWmNone); + 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, TwinWmNone); + 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, TwinWmNone); + 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); +} + +static void test_shadow(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + t->screen->background_color = 0xffffffff; + 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); +} + +static void test_rgb16_and_opacity(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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"); +#if defined(CONFIG_FONT_ICONS) + 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); +} + +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; +} + +static void test_tween(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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"); + + 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, TwinWmNone); + /* Far more distinct closures than the tracker holds at once */ + for (int i = 0; i < 4 * TWIN_MAX_CLOSURES; 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"); + + /* 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, TwinWmNone); + 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, TwinWmNone); + 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); +} + +static void test_leaks(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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); + 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_widget_t *widget, + twin_event_t *event, + void *closure) +{ + (void) widget; + (void) event; + (void) closure; + 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, TwinWmNone); + 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, TWIN_ARGB32, TwinWindowApplication, 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, 1, 1, + 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 void test_cleared_in_pass(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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 */ + tween_probe_t p = {.monotonic = true}; + twin_tween_t *tw = twin_tween_start(100, TwinEaseLinear, tween_probe, &p); + victim = NULL; + th_advance(t, 16); + int calls = p.calls; + twin_tween_cancel(tw); + 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, TwinWmNone); + 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, TwinWmNone); + 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, TWIN_ARGB32, TwinWindowApplication, 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); +} + +int main(void) +{ + test_damage_merge(); + test_occlusion_differential(); + test_row_fallback(); + test_shadow(); + test_rgb16_and_opacity(); + test_stub(); + test_cache_equivalence(); + test_text(); + test_gradient(); + test_tween(); + test_closure_churn(); + test_scroll(); + test_window_damage_rects(); + test_scaled_clip(); + test_cleared_in_pass(); + test_text_clip_overshoot(); + test_destroyed_paint(); + test_accessors(); +#if defined(CONFIG_WIDGETS) + test_widget_teardown(); +#endif + test_leaks(); + twin_cache_flush(); + return th_report("core"); +} diff --git a/tests/test-lifetime.c b/tests/test-lifetime.c new file mode 100644 index 00000000..d5bd633a --- /dev/null +++ b/tests/test-lifetime.c @@ -0,0 +1,341 @@ +/* + * 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, TwinWmNone); + 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, TwinWmNone); + twin_toplevel_t *top = twin_toplevel_create( + t->screen, TWIN_ARGB32, TwinWindowApplication, 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, TwinWmDesktop); + 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. + * Switching policy from that callback must skip the window, not crash. + */ +static void switch_wm_on_destroy(twin_window_t *w) +{ + CHECK(!w->pixmap, "pixels adopted for the pop animation"); + CHECK(twin_screen_set_wm(w->screen, TwinWmNone), "switch to 'none'"); +} + +static void test_wm_switch_from_destroy(void) +{ + th_t *t = th_create(W, H, TwinWmMobile); + 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 = switch_wm_on_destroy; + twin_window_destroy(b); + th_settle(t, 1000); + CHECK_EQ(twin_screen_get_wm(t->screen), TwinWmNone, "policy switched"); + CHECK(a->pixmap && a->pixmap->screen, "remaining app shown"); + CHECK_EQ(th_damage_check(t), 0, "screen consistent after the switch"); + + 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, TwinWmNone); + 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(); +} + +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_wm_switch_from_destroy(); +#endif + test_tween_finish_from_proc(); + 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..852b601f --- /dev/null +++ b/tests/test-wm.c @@ -0,0 +1,1306 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Window manager tests: placement and input under each policy, mobile + * transitions and eviction, and switching policies at run time. 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) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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); +} + +static void test_desktop(void) +{ +#if defined(CONFIG_WM_DESKTOP) + th_t *t = th_create(W, H, TwinWmDesktop); + 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, TwinWmDesktop); + 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, TwinWmNone); + 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"); + CHECK_EQ(p->shadow.blur, 6, "shadow carried"); + 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, TwinWmMobile); + 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 */ + +static void test_switching(void) +{ + th_t *t = th_create(W, H, TwinWmNone); + app_t bars[2] = {{.color = 0xff111111}, {.color = 0xff222222}}; + twin_window_t *bt = mk(t, &bars[0], TwinWindowPlain, 0, 0, W, TOP_BAR); + twin_window_t *bb = mk(t, &bars[1], TwinWindowPlain, 0, 0, W, BOTTOM_BAR); + twin_screen_set_bars(t->screen, bt, bb); + th_frame(t); + size_t base = th_heap_net(t); + + app_t apps[3] = { + {.color = 0xffaa0000}, {.color = 0xff00aa00}, {.color = 0xff0000aa}}; + twin_window_t *w[3]; + for (int i = 0; i < 3; i++) + w[i] = mk(t, &apps[i], TwinWindowApplication, 10, 30, 160, 200); + th_settle(t, 1000); + CHECK(!bt->pixmap->screen, "bars hidden without the mobile WM"); + CONSISTENT(t, "switch: none"); + + const twin_wm_mode_t cycle[] = {TwinWmMobile, TwinWmDesktop, TwinWmNone, + TwinWmDesktop, TwinWmMobile, TwinWmNone}; + for (size_t i = 0; i < sizeof(cycle) / sizeof(cycle[0]); i++) { + twin_wm_mode_t m = cycle[i]; + bool ok = twin_screen_set_wm(t->screen, m); +#if !defined(CONFIG_WM_MOBILE) + if (m == TwinWmMobile) { + CHECK(!ok, "compiled-out mobile WM refused"); + continue; + } +#endif +#if !defined(CONFIG_WM_DESKTOP) + if (m == TwinWmDesktop) { + CHECK(!ok, "compiled-out desktop WM refused"); + continue; + } +#endif + CHECK(ok, "switch to mode %d", m); + CHECK(twin_screen_get_wm(t->screen) == m, "mode %d active", m); + th_settle(t, 1000); + switch (m) { + case TwinWmMobile: + CHECK(shown(bt) && shown(bb), "mobile shows the bars"); + CHECK(shown(w[2]) && !w[0]->pixmap->screen, + "mobile shows only the top app"); + CHECK(w[2]->pixmap->y == TOP_BAR, "top app in the work area"); + break; + case TwinWmDesktop: + CHECK(!bt->pixmap->screen, "desktop hides the bars"); + CHECK(shown(w[0]) && shown(w[1]) && shown(w[2]), + "desktop shows every app"); + CHECK(w[2]->client.top > 0, "desktop decorations"); + CHECK(w[1]->pixmap->x != w[0]->pixmap->x, + "identical requests cascade"); + CHECK(w[2]->active, "topmost app active"); + break; + case TwinWmNone: + CHECK(w[2]->client.top == 0 && w[2]->pixmap->width == W, + "none: undecorated full screen"); + CHECK(t->screen->top == w[2]->pixmap, "stacking order kept"); + break; + } + CONSISTENT(t, "switch: after mode change"); + CHECK(th_pixel(t, W / 2, H / 2) == 0xff0000aa || m == TwinWmDesktop, + "top app visible after switching to %d", m); + } + +#if defined(CONFIG_WM_MOBILE) + /* Switching in the middle of a transition */ + twin_screen_set_wm(t->screen, TwinWmMobile); + th_settle(t, 1000); + app_t extra = {.color = 0xffaaaa00}; + twin_window_t *we = mk(t, &extra, TwinWindowApplication, 0, 0, W, H); + th_advance(t, 48); + twin_screen_set_wm(t->screen, TwinWmDesktop); + th_settle(t, 1000); + CONSISTENT(t, "switch: mid transition"); + twin_screen_set_wm(t->screen, TwinWmMobile); + twin_window_destroy(we); + th_advance(t, 48); + twin_screen_set_wm(t->screen, TwinWmNone); + th_settle(t, 1000); + CONSISTENT(t, "switch: mid pop"); +#endif + + for (int i = 0; i < 3; i++) + twin_window_destroy(w[i]); + th_settle(t, 1000); + CHECK(th_heap_net(t) <= base, "switching leaks nothing (%zu > %zu)", + th_heap_net(t), base); + twin_window_destroy(bt); + twin_window_destroy(bb); + th_destroy(t); +} + +#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) +static twin_wm_mode_t switch_to; +static bool dispatch_result; + +static bool switching_event(twin_window_t *w, twin_event_t *ev) +{ + if (ev->kind == TwinEventButtonDown) { + /* Switch while our own event is being delivered */ + dispatch_result = twin_screen_set_wm(w->screen, switch_to); + return true; + } + return app_event(w, ev); +} +#endif + +static void test_switch_from_handler(void) +{ +#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) + th_t *t = th_create(W, H, TwinWmMobile); + app_t a = {.color = 0xffaa3333}; + twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); + w->event = switching_event; + th_settle(t, 1000); + + switch_to = TwinWmDesktop; + th_press(t, 100, 100); + CHECK(twin_screen_get_wm(t->screen) == TwinWmDesktop, + "pending switch reported at once"); + th_release(t, 100, 100); + th_settle(t, 1000); + CHECK(t->screen->wm->mode == TwinWmDesktop, + "switch applied after the event"); + CHECK(w->client.top > 0, "window decorated after the switch"); + CONSISTENT(t, "switch from handler"); + + switch_to = TwinWmMobile; + th_tap(t, 100, 150); + th_settle(t, 1000); + CHECK(t->screen->wm->mode == TwinWmMobile, "switched back from a handler"); + twin_window_destroy(w); + + /* Phone-sized screens built for the work area cascade and stay on screen */ + twin_screen_set_wm(t->screen, TwinWmDesktop); + app_t apps[3] = { + {.color = 0xff111111}, {.color = 0xff222222}, {.color = 0xff333333}}; + twin_window_t *ws[3]; + for (int i = 0; i < 3; i++) + ws[i] = mk(t, &apps[i], TwinWindowApplication, 0, 20, W, 258); + twin_screen_set_wm(t->screen, TwinWmMobile); + twin_screen_set_wm(t->screen, TwinWmDesktop); + th_settle(t, 1000); + for (int i = 0; i < 3; i++) { + twin_pixmap_t *p = ws[i]->pixmap; + CHECK(p->x >= 0 && p->y >= 0 && p->x + p->width <= W && + p->y + p->height <= H, + "window %d on screen (%d,%d %dx%d)", i, p->x, p->y, p->width, + p->height); + } + CHECK(ws[0]->pixmap->y != ws[1]->pixmap->y, "second window cascaded"); + CONSISTENT(t, "cascade on a small screen"); + for (int i = 0; i < 3; i++) + twin_window_destroy(ws[i]); + th_destroy(t); +#endif +} + + +/* 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) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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); + +#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) +{ + th_t *t = th_create(W, H, TwinWmNone); + 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); +} + +#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, TwinWmDesktop); + 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, TwinWmDesktop); + 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, TwinWmDesktop); + 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 + +static void test_switch_pending(void) +{ +#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) + th_t *t = th_create(W, H, TwinWmMobile); + app_t a = {.color = 0xffaa3333}; + twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); + w->event = switching_event; + th_settle(t, 1000); + + /* A direct request replaces one queued by a handler */ + switch_to = TwinWmDesktop; + twin_event_t ev = {.kind = TwinEventButtonDown}; + ev.u.pointer.screen_x = 100; + ev.u.pointer.screen_y = 100; + twin_screen_dispatch(t->screen, &ev); + CHECK(twin_screen_get_wm(t->screen) == TwinWmDesktop, "queued"); + CHECK(twin_screen_set_wm(t->screen, TwinWmNone), "direct switch"); + th_settle(t, 1000); + CHECK(t->screen->wm->mode == TwinWmNone, "stale queued switch dropped"); + th_release(t, 100, 100); + + /* Switching while a swipe is under the finger */ + twin_screen_set_wm(t->screen, TwinWmMobile); + w->event = app_event; + app_t b = {.color = 0xff33aa33}; + twin_window_t *wb = mk(t, &b, TwinWindowApplication, 0, 20, W, 258); + th_settle(t, 1000); + th_press(t, 3, 150); + th_move(t, 60, 150); + twin_screen_set_wm(t->screen, TwinWmDesktop); + th_move(t, 90, 150); + th_release(t, 90, 150); + th_settle(t, 1000); + CONSISTENT(t, "switch mid swipe"); + twin_window_destroy(wb); + +#if defined(CONFIG_MEMORY_STATS) + /* Out of memory: a handler's switch is refused, not forced */ + twin_screen_set_wm(t->screen, TwinWmMobile); + th_settle(t, 1000); + w->event = switching_event; + switch_to = TwinWmDesktop; + _twin_alloc_fail_next = 1; + dispatch_result = true; + th_tap(t, 100, 100); + _twin_alloc_fail_next = 0; + CHECK(!dispatch_result, "queuing failure reported"); + th_settle(t, 1000); + CHECK(t->screen->wm->mode == TwinWmMobile, "current policy kept"); + + /* A policy that cannot start falls back to none */ + _twin_alloc_fail_next = 1; + CHECK(!twin_screen_set_wm(t->screen, TwinWmDesktop), "failed start"); + _twin_alloc_fail_next = 0; + CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "fell back to none"); + th_settle(t, 1000); + CONSISTENT(t, "after failed switches"); +#endif + twin_window_destroy(w); + 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(); + test_switching(); + test_switch_from_handler(); + test_switch_pending(); + twin_cache_flush(); + return th_report("wm"); +} diff --git a/tools/headless-ctl.c b/tools/headless-ctl.c index bcd18e6e..7e4e890d 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,71 @@ 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 */ +static int inject_text(twin_headless_shm_t *shm, const char *text) +{ + const unsigned char *p = (const unsigned char *) text; + while (*p) { + twin_ucs4_t c; + int len = utf8_decode(p, &c); + if (!len) { + fprintf(stderr, "Invalid UTF-8 sequence at byte %td\n", + (const char *) p - text); + return -1; + } + if (shm->event_write_idx - shm->event_read_idx >= + TWIN_HEADLESS_MAX_EVENTS) { + fprintf(stderr, "Event queue full - backend may be stalled\n"); + return -1; + } + 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++; + p += len; + } + return 0; +} + static void print_usage(const char *prog) { printf("Usage: %s [options] command\n", prog); @@ -373,6 +467,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 +593,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..3ef5d69c 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. + */ +extern unsigned _twin_debug; /* internal switch, see TWIN_DEBUG_NO_OCCLUSION */ +#define DEBUG_NO_OCCLUSION 0x1 + +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); + 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); + + /* 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 |= DEBUG_NO_OCCLUSION; + run_test_series("scan-out 240x320, 3 layers, no culling", test_scanout, 1, + 1); + _twin_debug &= ~DEBUG_NO_OCCLUSION; + run_test_series("scan-out 240x320, 3 layers, culled", test_scanout, 1, 1); + 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 @@ -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..b4dd29f9 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,38 @@ 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) -{ - char c; +#define MAX_UCS4 0x110000 +#define MAX_RANGES 64 - 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'; -} +static FT_ULong range_lo[MAX_RANGES], range_hi[MAX_RANGES]; +static int n_ranges; -static char *facename(FT_Face face) +static int wanted(FT_ULong ucs4) { - 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; + 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; } -#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 *dash = strchr(tok + 1, '-'); + if (n_ranges == MAX_RANGES) + return 0; + range_lo[n_ranges] = strtoul(tok, NULL, 0); + range_hi[n_ranges] = + dash ? strtoul(dash + 1, NULL, 0) : range_lo[n_ranges]; + n_ranges++; + } + 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 +201,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); @@ -240,22 +261,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 +291,47 @@ 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]; + } 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; } From 15882ff8b318994cca054d9bd9c7814e97fda720 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 18:02:13 +0800 Subject: [PATCH 02/48] Fix tween finish, TTF metrics, Pixman transforms twin_tween_finish() called from a step callback now always delivers the final frame, even when the callback also returns false. TrueType glyph metrics report bearings from the outline instead of zero and the advance, and only stroke fonts claim U+0000. The Pixman renderer passes the whole source transform, translation included; it used to keep only the scale terms. The injected allocation failure now covers realloc. The harness no longer overshoots th_settle's budget, frees the screen when th_create fails, fails on a missing or malformed golden file instead of skipping it, and its CHECK macros use temporaries no test can shadow. The timeout-cancels-tween test now cancels from a timeout due in the same pass, as its comment promised. --- src/anim.c | 5 ++- src/cache.c | 2 + src/draw-pixman.c | 19 ++++---- src/font.c | 55 ++++++++++++++++++++--- src/memstats.c | 4 ++ tests/harness.c | 38 ++++++++++++---- tests/harness.h | 24 +++++----- tests/test-core.c | 109 +++++++++++++++++++++++++++++++++++++++++++--- 8 files changed, 218 insertions(+), 38 deletions(-) diff --git a/src/anim.c b/src/anim.c index 9e886b7e..d45de0ee 100644 --- a/src/anim.c +++ b/src/anim.c @@ -78,8 +78,9 @@ static twin_time_t _twin_tween_tick(twin_time_t now, void *closure) tween->running = true; bool keep = (*tween->proc)(value, tween->closure); - /* twin_tween_finish() from inside proc still promises the end value */ - if (tween->finished && !last && keep) + /* 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; diff --git a/src/cache.c b/src/cache.c index 377e6982..ac952af9 100644 --- a/src/cache.c +++ b/src/cache.c @@ -108,6 +108,8 @@ bool _twin_cache_enabled(void) 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) { diff --git a/src/draw-pixman.c b/src/draw-pixman.c index 2c30e32d..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); diff --git a/src/font.c b/src/font.c index f8302a6a..88e609b5 100644 --- a/src/font.c +++ b/src/font.c @@ -226,9 +226,10 @@ bool twin_has_ucs4(twin_font_t *font, twin_ucs4_t ucs4) { if (!twin_find_ucs4_page(font, twin_ucs_page(ucs4))) return false; - /* Offset 0 marks a code point the font does not cover */ + /* 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; + (ucs4 == 0 && font->type == TWIN_FONT_TYPE_STROKE); } #define SNAPX(p) _snap(path, p, snap_x, nsnap_x) @@ -277,6 +278,44 @@ static twin_fixed_t _twin_glyph_width(twin_text_info_t *info, return width; } +/* + * 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) +{ + 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; @@ -297,10 +336,16 @@ static void twin_text_metrics_ucs4(twin_path_t *path, _twin_text_compute_info(path, font, &info); if (_twin_font_is_ttf(font)) { - /* TrueType: advance only; vertical metrics come from the font */ + /* TrueType: bearings from the outline; vertical metrics from the font + */ twin_fixed_t adv = FX(b[0], &info); - m->left_side_bearing = 0; - m->right_side_bearing = adv; + 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); diff --git a/src/memstats.c b/src/memstats.c index 4aa66d0d..cfe6cf80 100644 --- a/src/memstats.c +++ b/src/memstats.c @@ -179,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/tests/harness.c b/tests/harness.c index d7d0dd73..0609d96c 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -40,6 +40,8 @@ th_t *th_create(twin_coord_t width, twin_coord_t height, twin_wm_mode_t wm) 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; @@ -94,12 +96,15 @@ void th_advance(th_t *t, int ms) void th_settle(th_t *t, int max_ms) { - for (int spent = 0; spent < max_ms; spent += 16) { + 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; - virtual_now += 16; + /* Never step past the budget */ + int step = max_ms - spent < 16 ? max_ms - spent : 16; + virtual_now += step; + spent += step; } th_frame(t); } @@ -381,15 +386,22 @@ static const char *golden_dir(void) 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, - bool *config_ok) + 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_ok = false; + *config = GOLDEN_UNAVAILABLE; if (!f) return false; bool found = false; @@ -397,7 +409,8 @@ static bool golden_lookup(const char *suite, char key[64]; unsigned v; if (sscanf(line, "# config %x", &v) == 1) { - *config_ok = v == config_fingerprint(); + *config = v == config_fingerprint() ? GOLDEN_SAME_CONFIG + : GOLDEN_OTHER_CONFIG; continue; } if (sscanf(line, "%63s %x", key, &v) == 2 && !strcmp(key, name)) { @@ -429,12 +442,21 @@ void th_golden(const char *suite, const char *name, uint32_t crc, const th_t *t) } uint32_t want = 0; - bool config_ok; - bool found = golden_lookup(suite, name, &want, &config_ok); - if (!config_ok) { + 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) { diff --git a/tests/harness.h b/tests/harness.h index a4381340..5051d4d3 100644 --- a/tests/harness.h +++ b/tests/harness.h @@ -96,19 +96,23 @@ extern int th_checks, th_failures; } \ } while (0) -/* Evaluates each operand once (they may have side effects) */ -#define CHECK_EQ(a, b, what) \ - do { \ - long long _a = (long long) (a), _b = (long long) (b); \ - CHECK(_a == _b, "%s: got %lld, expected %lld", what, _a, _b); \ +/* 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 *_s = (a), *_t = (b); \ - CHECK(_s && !strcmp(_s, _t), "%s: got \"%s\", expected \"%s\"", what, \ - _s ? _s : "(null)", _t); \ +#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 */ diff --git a/tests/test-core.c b/tests/test-core.c index d61a9b8a..34f04ba5 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -400,7 +400,25 @@ static void test_text(void) 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, @@ -461,6 +479,23 @@ static bool tween_probe(twin_fixed_t v, void *closure) 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, TwinWmNone); @@ -494,6 +529,16 @@ static void test_tween(void) 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); @@ -697,6 +742,28 @@ static void test_scaled_clip(void) 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, TwinWmNone); @@ -716,6 +783,23 @@ static void test_leaks(void) 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); } @@ -792,6 +876,16 @@ static twin_time_t victim_proc(twin_time_t now, void *closure) 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, TwinWmNone); @@ -802,13 +896,17 @@ static void test_cleared_in_pass(void) 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 */ + /* 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}; - twin_tween_t *tw = twin_tween_start(100, TwinEaseLinear, tween_probe, &p); - victim = NULL; - th_advance(t, 16); + 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; - twin_tween_cancel(tw); + 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); @@ -984,6 +1082,7 @@ int main(void) test_scroll(); test_window_damage_rects(); test_scaled_clip(); + test_translated_source(); test_cleared_in_pass(); test_text_clip_overshoot(); test_destroyed_paint(); From d57ceb87bf25bb86205de8d91401caa68fc00571 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 18:02:13 +0800 Subject: [PATCH 03/48] Fix phone gesture, tween and calculator edge cases Home and lock screen drags that interrupt a settle animation continue from where the page or knob is, and a tween that cannot start snaps to its end. A press that stops a fling inside a list no longer becomes a tap. phone_boot() fails cleanly when the mobile policy, a bar or home cannot be created. The calculator divides without overflowing on large operands and lets +/- start a negative entry. Text truncation breaks overlong words at a character boundary, icon detection only matches the private-use range, music position math survives clock wrap, and the page dots are centered (the home and dark-theme home goldens move by 2 px). --- apps/cellphone/calc.c | 64 ++++++++++++++++++++++++++++++++---- apps/cellphone/camera.c | 3 +- apps/cellphone/contacts.c | 6 +++- apps/cellphone/games.c | 6 ++-- apps/cellphone/home.c | 22 ++++++++++--- apps/cellphone/lock.c | 7 ++-- apps/cellphone/music.c | 19 ++++++++--- apps/cellphone/phone.h | 2 ++ apps/cellphone/shell.c | 16 ++++++--- apps/cellphone/ui-internal.h | 3 +- apps/cellphone/ui-paint.c | 16 +++++++-- apps/cellphone/ui.c | 36 +++++++++++++++----- tests/golden/cellphone.txt | 4 +-- tests/phone-music.c | 3 +- 14 files changed, 167 insertions(+), 40 deletions(-) diff --git a/apps/cellphone/calc.c b/apps/cellphone/calc.c index 4ea47235..eb64160e 100644 --- a/apps/cellphone/calc.c +++ b/apps/cellphone/calc.c @@ -50,6 +50,7 @@ typedef struct { 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; @@ -107,6 +108,44 @@ static bool scaled_mul(int64_t a, int64_t b, int64_t *res) 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) { @@ -123,10 +162,8 @@ static bool execute(calc_t *c) goto fail; break; case '/': - if (!c->operand || c->accumulator > INT64_MAX / SCALE || - c->accumulator < INT64_MIN / SCALE) + if (!scaled_div(c->accumulator, c->operand, &c->accumulator)) goto fail; - c->accumulator = c->accumulator * SCALE / c->operand; break; default: c->accumulator = c->operand; @@ -147,7 +184,7 @@ static void reset(calc_t *c) c->pending = 0; c->new_input = true; c->frac_div = 0; - c->negative = c->error = false; + c->negative = c->fresh = c->error = false; strcpy(c->display, "0"); } @@ -164,7 +201,11 @@ static void append_digit(calc_t *c, char digit) if (c->frac_div > SCALE) return; size_t len = strlen(c->display); - if (len >= BUF_LEN - 2 || (!c->frac_div && len >= 12)) + /* 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; @@ -204,6 +245,15 @@ static void press_key(calc_t *c, int i) 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; @@ -215,6 +265,7 @@ static void press_key(calc_t *c, int i) 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] == '.') @@ -229,6 +280,7 @@ static void press_key(calc_t *c, int i) c->pending = 0; format_scaled(c, c->operand); c->new_input = true; + c->fresh = false; c->frac_div = 0; } } else { @@ -242,7 +294,7 @@ static void press_key(calc_t *c, int i) c->accumulator = c->operand; } c->pending = op; - c->new_input = true; + c->new_input = c->fresh = true; c->frac_div = 0; } } diff --git a/apps/cellphone/camera.c b/apps/cellphone/camera.c index 67d52053..22978c6d 100644 --- a/apps/cellphone/camera.c +++ b/apps/cellphone/camera.c @@ -475,7 +475,8 @@ static bool camera_event(ui_view_t *v, const ui_event_t *e) case UI_RELEASE: set_pressed(c, CTL_NONE); /* Horizontal swipes over the viewfinder switch modes */ - if (e->dx < -40 || e->dx > 40) + 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: diff --git a/apps/cellphone/contacts.c b/apps/cellphone/contacts.c index 9fbec2da..baaab4c8 100644 --- a/apps/cellphone/contacts.c +++ b/apps/cellphone/contacts.c @@ -248,7 +248,11 @@ static void list_added(contacts_list_t *l, int idx) l->scroll.offset = off < 0 ? 0 : off; l->scroll.flinging = false; l->fresh = idx; - ui_view_every(&l->view, FRESH_MS, fresh_done); + /* 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); } diff --git a/apps/cellphone/games.c b/apps/cellphone/games.c index 38843e89..89436c64 100644 --- a/apps/cellphone/games.c +++ b/apps/cellphone/games.c @@ -211,8 +211,10 @@ 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) + /* Freeze while another view covers this one. Only the mobile scene + * stacks views exclusively: desktop windows may sit side by side. */ + if (g->paused || (phone_top() != v && + twin_screen_get_wm(phone_screen()) != TwinWmDesktop)) return o->period; o->tick(g); g->steps++; diff --git a/apps/cellphone/home.c b/apps/cellphone/home.c index 8febe75d..aaf3a3d3 100644 --- a/apps/cellphone/home.c +++ b/apps/cellphone/home.c @@ -32,7 +32,9 @@ typedef struct { 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; - int pressed; /* app index under the finger, -1 when none */ + 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) @@ -108,6 +110,8 @@ 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; } @@ -130,10 +134,17 @@ static bool home_event(ui_view_t *v, const ui_event_t *e) 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); - twin_coord_t pan = h->page * PHONE_W - e->dx; + 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) @@ -152,8 +163,11 @@ static bool home_event(ui_view_t *v, const ui_event_t *e) h->page = page; h->from = h->pan; h->to = page * PHONE_W; - if (h->from != h->to) - ui_view_tween(v, 220, TwinEaseOutCubic, settle_step); + 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: { diff --git a/apps/cellphone/lock.c b/apps/cellphone/lock.c index ee50e682..f315e8cf 100644 --- a/apps/cellphone/lock.c +++ b/apps/cellphone/lock.c @@ -130,6 +130,9 @@ static bool lock_event(ui_view_t *v, const ui_event_t *e) 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: @@ -147,8 +150,8 @@ static bool lock_event(ui_view_t *v, const ui_event_t *e) return true; } l->from = l->knob; - if (l->from) - ui_view_tween(v, 180, TwinEaseOutCubic, spring_back); + if (l->from && !ui_view_tween(v, 180, TwinEaseOutCubic, spring_back)) + set_knob(l, 0); return true; default: return false; diff --git a/apps/cellphone/music.c b/apps/cellphone/music.c index 0c3f6501..d22c6d73 100644 --- a/apps/cellphone/music.c +++ b/apps/cellphone/music.c @@ -44,9 +44,20 @@ 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 ? (int32_t) (twin_now() - player.start) : player.pos; + return player.playing ? player_elapsed() : player.pos; } static int shuffle_pick(void) @@ -72,8 +83,8 @@ static void player_sync(void) { if (!player.playing) return; - while ((int32_t) (twin_now() - player.start) >= track_ms(player.track)) { - player.start += track_ms(player.track); + while (player_elapsed() >= track_ms(player.track)) { + player.start = time_add(player.start, track_ms(player.track)); if (!player.repeat) player.track = next_track(); } @@ -87,7 +98,7 @@ static void player_seek(int32_t ms) if (ms > dur - 1) ms = dur - 1; if (player.playing) - player.start = twin_now() - ms; + player.start = time_add(twin_now(), -ms); else player.pos = ms; } diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index e0478020..99f8dbde 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -453,6 +453,7 @@ 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, @@ -545,6 +546,7 @@ typedef struct { 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); diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 3ce41a20..c4ab0173 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -673,8 +673,11 @@ bool phone_boot(twin_screen_t *screen) if (!ui_init()) return false; g_screen = screen; - if (twin_screen_get_wm(screen) != TwinWmMobile) - twin_screen_set_wm(screen, TwinWmMobile); + if (twin_screen_get_wm(screen) != TwinWmMobile && + !twin_screen_set_wm(screen, TwinWmMobile)) { + phone_shutdown(); + return false; + } 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); @@ -683,10 +686,13 @@ bool phone_boot(twin_screen_t *screen) g_tick = twin_set_timeout(status_tick, 1000, g_status); g_home = home_create(); - if (g_home) { - g_home->destroy = home_destroyed; - ui_view_show(g_home); + 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; } diff --git a/apps/cellphone/ui-internal.h b/apps/cellphone/ui-internal.h index 12515571..5a5e1033 100644 --- a/apps/cellphone/ui-internal.h +++ b/apps/cellphone/ui-internal.h @@ -22,7 +22,8 @@ /* 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; + 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 index 519b9e82..183885af 100644 --- a/apps/cellphone/ui-paint.c +++ b/apps/cellphone/ui-paint.c @@ -138,6 +138,17 @@ int ui_text_wrap(twin_pixmap_t *px, 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; @@ -184,7 +195,7 @@ void ui_text_fit(twin_pixmap_t *px, ui_text_in(px, size, r, align, color, s); return; } - char buf[96]; + char buf[96] = "..."; size_t n = strlen(s); if (n > sizeof(buf) - 4) n = sizeof(buf) - 4; @@ -635,7 +646,8 @@ void ui_dots(twin_pixmap_t *px, int current) { const phone_theme_t *th = phone_theme(); - int total = count * 10 + 8; + 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) { diff --git a/apps/cellphone/ui.c b/apps/cellphone/ui.c index e8766f4e..e94ecea1 100644 --- a/apps/cellphone/ui.c +++ b/apps/cellphone/ui.c @@ -288,11 +288,7 @@ static void _ui_destroy(twin_window_t *window) if (v->alive) *v->alive = false; ui_view_stop_timers(v); - for (int i = 0; i < UI_VIEW_TIMERS; i++) { - if (v->tweens[i].handle) - twin_tween_cancel(v->tweens[i].handle); - v->tweens[i].handle = NULL; - } + ui_view_stop_tweens(v); if (fling_view == v) { fling_view = NULL; fling_scroll = NULL; @@ -357,7 +353,9 @@ static bool _tween(twin_fixed_t value, void *closure) return false; if (slot->handle != self) return false; /* cancelled from the callback, slot maybe reused */ - if (!keep || value >= TWIN_FIXED_ONE) { + /* 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; } @@ -402,6 +400,15 @@ void ui_view_stop_timers(ui_view_t *view) } } +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, @@ -479,11 +486,20 @@ 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; - return true; /* a press that stops a fling is not a tap */ + /* 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; @@ -500,7 +516,11 @@ bool ui_scroll_event(ui_view_t *view, ui_scroll_t *s, const ui_event_t *e) fling_scroll->flinging = false; fling_scroll = s; fling_view = view; - ui_view_every(view, 16, _fling_step); + if (!ui_view_every(view, 16, _fling_step)) { + s->flinging = false; + fling_scroll = NULL; + fling_view = NULL; + } } return true; default: diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index f4ba5cd8..9029a641 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -2,7 +2,7 @@ # Regenerate with: make check-update # config 2336132b lock 052098fa -home 1a3dc03b +home 5ede29ca calculator 749994d4 dialer-empty 7e87b318 dialer-number 13143cec @@ -48,7 +48,7 @@ settings-sound 6b5df6cf settings-wallpaper 7a91c018 settings-time 5b4ffa59 settings-display-dark 85a1f340 -settings-home-dark e14d1f87 +settings-home-dark e8e63c74 settings-sound-changed 54b18173 settings-wallpaper-dusk d9c420f3 settings-wm 9ed9fd7e diff --git a/tests/phone-music.c b/tests/phone-music.c index fe1e51db..79f72613 100644 --- a/tests/phone-music.c +++ b/tests/phone-music.c @@ -145,7 +145,6 @@ void test_music(void) phone_back(); ph_wait(2000); CHECK_EQ(phone_depth(), 1, "back to the list"); - int track = app_music_test_track(); ph_shot("music-list-playing"); /* Mini player toggles playback without leaving the list */ @@ -166,7 +165,7 @@ void test_music(void) 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(track != last || app_music_test_playing(), "playing"); + CHECK(app_music_test_playing(), "tapped song is playing"); /* The player outlives the app: playback continues while closed */ ph_home(); From 7a8300ffff569cbbf85a0c0d02b886c35d433316 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 18:02:13 +0800 Subject: [PATCH 04/48] Harden headless control, font tool and tests The headless backend records its pid in the shared memory header and refuses to start while another live backend owns it, instead of unlinking it; a stale object from a crashed run is still replaced. headless-ctl validates UTF-8 before typing anything and waits for the event queue to drain, so long strings work. The DRM backend forgets its mapping when it unmaps the framebuffer. twin-ttf validates the font name and range syntax and refuses a range set with no glyphs. mado-perf restores the cache budget it changes and uses the shared debug flags. make clean removes the test binaries, check-update reports failures, and check explains that the headless tests do not run under Emscripten. --- Makefile | 11 ++++++++-- backend/drm.c | 6 ++++-- backend/headless-shm.h | 2 ++ backend/headless.c | 44 +++++++++++++++++++++++++++++++++++---- backend/sdl.c | 2 +- tools/headless-ctl.c | 30 +++++++++++++++++++++------ tools/perf.c | 16 +++++++------- tools/ttf/twin-ttf.c | 47 ++++++++++++++++++++++++++++++++++++++---- 8 files changed, 131 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 2947f0dd..5bb7c10e 100644 --- a/Makefile +++ b/Makefile @@ -312,12 +312,13 @@ 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_CELLPHONE), y) TESTS += test-cellphone endif -ifneq ($(filter check check-update,$(MAKECMDGOALS)),) +ifneq ($(filter check check-update clean distclean,$(MAKECMDGOALS)),) ifneq ($(CC_IS_EMCC), 1) define test-target target-y += $(1) @@ -335,13 +336,19 @@ endif 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 - @for t in $(TESTS); do MADO_UPDATE_GOLDEN=1 ./$$t; done + @status=0; for t in $(TESTS); do MADO_UPDATE_GOLDEN=1 ./$$t || status=1; done; exit $$status +endif # Build system integration diff --git a/backend/drm.c b/backend/drm.c index 7df546d8..5a1add8e 100644 --- a/backend/drm.c +++ b/backend/drm.c @@ -166,8 +166,10 @@ static bool create_fb(twin_drm_t *tx, int32_t crtc_id) static void destroy_fb(twin_drm_t *tx) { - /* Unmap framebuffer */ - munmap(tx->fb_base, tx->fb_len); + /* Unmap framebuffer; MAP_FAILED tells the work loop there is no mapping */ + if (tx->fb_base != MAP_FAILED) + munmap(tx->fb_base, tx->fb_len); + tx->fb_base = MAP_FAILED; /* Delete framebuffer */ drmModeRmFB(tx->drm_dri_fd, tx->fb_id); diff --git a/backend/headless-shm.h b/backend/headless-shm.h index 084a27f8..e2efb5fa 100644 --- a/backend/headless-shm.h +++ b/backend/headless-shm.h @@ -42,6 +42,8 @@ typedef struct { 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; diff --git a/backend/headless.c b/backend/headless.c index c01112ea..99ef06e9 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -319,6 +319,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)); @@ -340,12 +364,23 @@ twin_context_t *twin_headless_init(int width, int height) if (!tx->shm_size) goto error; - /* Create shared memory, readable and writable by this user only. A - * stale object from a crashed run is removed first; O_EXCL then makes - * sure the backend never adopts one somebody else created. */ - shm_unlink(TWIN_HEADLESS_SHM_NAME); + /* 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)); @@ -374,6 +409,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; diff --git a/backend/sdl.c b/backend/sdl.c index 6dfe0fab..51ff040d 100644 --- a/backend/sdl.c +++ b/backend/sdl.c @@ -229,7 +229,7 @@ static bool twin_sdl_poll(twin_context_t *ctx) twin_sdl_t *tx = PRIV(ctx); /* Sleep until input arrives or the next timeout is due instead of - * spinning: an idle screen should cost no CPU. */ + * spinning, waking at least every 100 ms */ if (!twin_screen_damaged(screen)) { twin_time_t wait = _twin_timeout_delay(); if (wait < 0 || wait > 100) diff --git a/tools/headless-ctl.c b/tools/headless-ctl.c index 7e4e890d..d579c0af 100644 --- a/tools/headless-ctl.c +++ b/tools/headless-ctl.c @@ -432,29 +432,47 @@ static int utf8_decode(const unsigned char *p, twin_ucs4_t *c) return len; } -/* Queue each UTF-8 character of @text as a UCS-4 key event */ +/* + * 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) { - twin_ucs4_t c; int len = utf8_decode(p, &c); if (!len) { fprintf(stderr, "Invalid UTF-8 sequence at byte %td\n", (const char *) p - text); return -1; } - if (shm->event_write_idx - shm->event_read_idx >= - TWIN_HEADLESS_MAX_EVENTS) { - fprintf(stderr, "Event queue full - backend may be stalled\n"); + 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++; - p += len; } return 0; } diff --git a/tools/perf.c b/tools/perf.c index 3ef5d69c..931c3937 100644 --- a/tools/perf.c +++ b/tools/perf.c @@ -309,9 +309,6 @@ static void run_large_tests(void) * off and on, and a phone-like screen scan-out with occlusion culling off * and on. */ -extern unsigned _twin_debug; /* internal switch, see TWIN_DEBUG_NO_OCCLUSION */ -#define DEBUG_NO_OCCLUSION 0x1 - static twin_path_t *vec_rrect; static twin_screen_t *vec_screen; static twin_pixmap_t *vec_layers[3]; @@ -354,6 +351,7 @@ 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(); @@ -370,6 +368,7 @@ static void run_vector_tests(void) 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); @@ -384,11 +383,12 @@ static void run_vector_tests(void) twin_pixmap_move(p, 0, i == 2 ? 0 : 20); twin_pixmap_show(p, vec_screen, vec_screen->top); } - _twin_debug |= DEBUG_NO_OCCLUSION; - run_test_series("scan-out 240x320, 3 layers, no culling", test_scanout, 1, - 1); - _twin_debug &= ~DEBUG_NO_OCCLUSION; - run_test_series("scan-out 240x320, 3 layers, culled", test_scanout, 1, 1); + _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); diff --git a/tools/ttf/twin-ttf.c b/tools/ttf/twin-ttf.c index b4dd29f9..bd0bbb4a 100644 --- a/tools/ttf/twin-ttf.c +++ b/tools/ttf/twin-ttf.c @@ -178,18 +178,47 @@ static int wanted(FT_ULong ucs4) return 0; } +/* 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 *stop; + + if (s == end || *s < '0' || *s > '9') + return 0; + *out = strtoul(s, &stop, 0); + return stop == end && *out < MAX_UCS4; +} + /* "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 *dash = strchr(tok + 1, '-'); + char *end = tok + strlen(tok); + char *dash = strchr(tok, '-'); if (n_ranges == MAX_RANGES) return 0; - range_lo[n_ranges] = strtoul(tok, NULL, 0); - range_hi[n_ranges] = - dash ? strtoul(dash + 1, NULL, 0) : range_lo[n_ranges]; + 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; } @@ -254,6 +283,12 @@ static int convert_font(const char *in_name, const char *symbol) } 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); @@ -319,6 +354,10 @@ int main(int argc, char **argv) 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]); From 07a45f47f4574c2331bc71f43096856923fa22ac Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 18:02:13 +0800 Subject: [PATCH 05/48] Correct backend and phone documentation Document the SDL scale range and its 100 ms idle wake-up, the headless-ctl type command, that eviction is optional, that Photos uses JPEG fixtures, and that full-recomposite checks run at test checkpoints. --- README.md | 7 ++++--- docs/backends.md | 7 ++++--- docs/phone.md | 10 ++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cbd65469..b1664ad2 100644 --- a/README.md +++ b/README.md @@ -140,11 +140,12 @@ for the design of the window manager, compositor and caches. ### Verification -Headless test suites run in-process with a virtual clock and compare every -incrementally updated frame against a full recomposite: +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 and phone suites +$ make check # core, window manager, lifetime and phone suites $ make check-update # re-record golden frame checksums ``` diff --git a/docs/backends.md b/docs/backends.md index 5c03d7ab..317ddb21 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -35,9 +35,9 @@ Run: Once the window appears, interact with the phone (the default scene) or, with `MADO_SCENE=desktop`, move windows and interact with widgets. 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. The backend sleeps -until input or the next timeout when nothing is damaged, and presents once -per screen update. +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 @@ -365,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) diff --git a/docs/phone.md b/docs/phone.md index ecff7d7a..eba5b8c7 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -3,8 +3,9 @@ This document describes how Mado runs a phone user interface and 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 entirely -with anti-aliased vector graphics on a CPU without graphics acceleration. +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 @@ -38,8 +39,9 @@ manager at all; the cellphone demo's Settings app switches between the three while running. Switching policies re-lays out every existing window: decorations are added -or removed, application windows are cascaded or stacked, and pixels of -windows the new policy keeps hidden are released. A switch requested from +or removed, application windows are cascaded or stacked, and, with +`WM_MOBILE_EVICT`, pixels of windows the new policy keeps hidden are +released. A switch requested from an event handler is applied once that event has been delivered; a later request replaces it, and if it cannot be queued (out of memory) the call fails and the current policy stays. `screen->windows` lists From 3ca1a99e37e12e9c4570458c2533e7082e647508 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 20:18:45 +0800 Subject: [PATCH 06/48] Return custom widget sizes in pixels twin_custom_widget_width() and twin_custom_widget_height() were declared to return twin_fixed_t but return the widget extents, which are whole pixels. The clock already treated the value as pixels; a caller that converted it with twin_fixed_to_int() got zero. Declare both as twin_coord_t and say so. --- include/twin.h | 8 ++++---- src/widget.c | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/twin.h b/include/twin.h index 56c70be5..611f1f91 100644 --- a/include/twin.h +++ b/include/twin.h @@ -994,11 +994,11 @@ void *twin_custom_widget_data(twin_custom_widget_t *custom); * 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); diff --git a/src/widget.c b/src/widget.c index cb10180f..55224a7e 100644 --- a/src/widget.c +++ b/src/widget.c @@ -235,12 +235,12 @@ void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source) _twin_widget_queue_layout(widget); } -static twin_fixed_t twin_widget_width(twin_widget_t *widget) +static twin_coord_t twin_widget_width(twin_widget_t *widget) { return _twin_widget_width(widget); } -static twin_fixed_t twin_widget_height(twin_widget_t *widget) +static twin_coord_t twin_widget_height(twin_widget_t *widget) { return _twin_widget_height(widget); } @@ -432,12 +432,12 @@ void *twin_custom_widget_data(twin_custom_widget_t *custom) return custom ? custom->data : NULL; } -twin_fixed_t twin_custom_widget_width(twin_custom_widget_t *custom) +twin_coord_t twin_custom_widget_width(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) +twin_coord_t twin_custom_widget_height(twin_custom_widget_t *custom) { return custom ? twin_widget_height(custom->widget) : 0; } From 561fa1f485f0eacea40260d8736eb848e57db210 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 20:18:50 +0800 Subject: [PATCH 07/48] Repaint every widget when a toplevel redraws A toplevel's draw callback sent Paint to its box, which repaints only the widgets queued since the last paint. When the mobile window manager restored an evicted toplevel, nothing was queued, so the window came back blank. Flag the whole widget tree before painting: the window asked for its damaged pixels and they may be all of them. --- src/box.c | 10 ++++++++++ src/toplevel.c | 4 ++++ src/twin_private.h | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/src/box.c b/src/box.c index 622a2122..cd4ffa1b 100644 --- a/src/box.c +++ b/src/box.c @@ -146,6 +146,16 @@ 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); + } +} + twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure) diff --git a/src/toplevel.c b/src/toplevel.c index a635daaf..26598e04 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); diff --git a/src/twin_private.h b/src/twin_private.h index 37ee2430..39e8be37 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1039,6 +1039,10 @@ 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); + twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_event_t *event, void *closure); From fb5fdd6a158b109dcbf99cdc74ce4a9e1f225496 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 20:19:04 +0800 Subject: [PATCH 08/48] Build one window manager, run all apps on it The desktop and mobile window managers are now a Kconfig choice: a build has one of them, or only 'none' (WM_NONE), and twin_screen_set_wm() switches between that one and 'none'. The default is the desktop, as on main; building both is a compile error. The phone no longer offers a window manager page in Settings, nor the header Back button that only the other scenes needed: it always runs under the mobile policy. apps/apps.c registers every demo app that is built together with the window managers it runs under. Ordinary apps are marked for all three; the phone shell, which owns the screen, for the mobile policy only. The demo launcher starts the scene's shell, or every app marked for the scene ('none' starts the first), and the phone's home screen lists every other app marked for mobile. Phone navigation (Back, Home, the nav bar) acts on the topmost application window whether or not it is a phone view, so these apps go back and home like phone apps, and a revealed view refreshes the nav bar. The apps now work at any size a policy gives them. The multi demo's windows paint from draw callbacks scaled to the client area, so they survive resizing and eviction; the animation centres its image; start functions report failure instead of using a NULL window. test-demos runs every registered app under each policy the build has that it is marked for, checking placement, consistency, that revealed windows are painted, closing window by window, and leaks. The phone suite opens each app from the launcher. test-wm switches between the built policy and 'none'. CI builds and tests both the desktop and the mobile configuration, under Valgrind and the sanitizers too. --- .github/workflows/headless-test.yml | 7 + .github/workflows/main.yml | 51 +++-- Makefile | 6 +- README.md | 33 +++- apps/animation.c | 31 ++- apps/apps.c | 121 ++++++++++++ apps/apps.h | 48 +++++ apps/apps_animation.h | 3 +- apps/apps_calc.h | 3 +- apps/apps_clock.h | 3 +- apps/apps_image.h | 3 +- apps/apps_multi.h | 3 +- apps/apps_spline.h | 3 +- apps/calc.c | 9 +- apps/cellphone/games.c | 6 +- apps/cellphone/phone.h | 9 +- apps/cellphone/settings.c | 97 +--------- apps/cellphone/shell.c | 144 ++++++++------ apps/cellphone/ui.c | 5 +- apps/clock.c | 5 +- apps/image.c | 5 +- apps/main.c | 142 ++++++-------- apps/multi.c | 280 ++++++++++++++++------------ apps/spline.c | 5 +- configs/Kconfig | 49 +++-- docs/backends.md | 4 +- docs/phone.md | 48 +++-- include/twin.h | 9 +- src/twin_private.h | 4 + tests/golden/cellphone.txt | 9 +- tests/golden/core.txt | 2 +- tests/harness.c | 13 +- tests/phone-demos.c | 134 +++++++++++++ tests/phone-scenes.c | 48 +++-- tests/phone-settings.c | 49 +---- tests/phone-test.h | 1 + tests/test-cellphone.c | 8 +- tests/test-demos.c | 154 +++++++++++++++ tests/test-wm.c | 73 +++++--- 39 files changed, 1075 insertions(+), 552 deletions(-) create mode 100644 apps/apps.c create mode 100644 apps/apps.h create mode 100644 tests/phone-demos.c create mode 100644 tests/test-demos.c diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index f3e39ad1..232504ff 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -165,6 +165,13 @@ jobs: 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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf74f307..24c91ed8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,7 +46,7 @@ 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 @@ -56,7 +56,22 @@ jobs: - name: headless tests under Valgrind run: | sudo apt-get install -y -q valgrind - for t in test-core test-wm test-lifetime test-cellphone; do + 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 @@ -74,10 +89,16 @@ jobs: sudo apt-get install -y libsdl2-dev libjpeg-dev libpng-dev - name: AddressSanitizer + UndefinedBehaviorSanitizer run: | - make defconfig - make check \ - CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined" \ - LDFLAGS="-fsanitize=address,undefined" + for wm in WM_DESKTOP WM_MOBILE; 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] @@ -88,24 +109,22 @@ jobs: matrix: include: - name: no window manager - options: WM_DESKTOP=n WM_MOBILE=n - - name: mobile only - options: WM_DESKTOP=n - - name: desktop only - options: WM_MOBILE=n + options: WM_NONE=y - name: no coverage cache - options: CACHE_SIZE=0 + options: WM_MOBILE=y CACHE_SIZE=0 - name: no mobile eviction - options: WM_MOBILE_EVICT=n + options: WM_MOBILE=y WM_MOBILE_EVICT=n - name: no allocator stats options: MEMORY_STATS=n - name: no widget toolkit - options: WIDGETS=n + options: WM_MOBILE=y WIDGETS=n - name: RGB565 phone surfaces - options: DEMO_CELLPHONE_RGB16=y + options: WM_MOBILE=y DEMO_CELLPHONE_RGB16=y - name: TLSF heap - options: MEM_TLSF=y MEM_TLSF_POOL_SIZE=4194304 + 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: config (${{ matrix.name }}) steps: diff --git a/Makefile b/Makefile index 5bb7c10e..fbbc1098 100644 --- a/Makefile +++ b/Makefile @@ -151,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 @@ -180,7 +181,7 @@ libapps.a_files-$(CONFIG_DEMO_CELLPHONE) += \ apps/cellphone/pong.c \ apps/cellphone/tetris.c -libapps.a_includes-y := include apps/cellphone +libapps.a_includes-y := include apps apps/cellphone # Emscripten size optimization libapps.a_cflags-$(CC_IS_EMCC) += -Oz @@ -314,6 +315,9 @@ 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 diff --git a/README.md b/README.md index b1664ad2..7d09ad57 100644 --- a/README.md +++ b/README.md @@ -124,19 +124,32 @@ Run the demo program: $ ./demo-sdl ``` -The default scene is a phone: a lock screen (drag the knob to unlock), a -home screen and a dozen apps drawn with vector graphics on the mobile window -manager. Drag from the left edge of an app to go back. Other scenes run -the same window system under a different window-manager policy: +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 drawn with vector graphics on the mobile window manager; +the launcher also lists every demo app that is built: ```shell -$ MADO_SCENE=desktop ./demo-sdl # overlapping, decorated windows -$ MADO_SCENE=none ./demo-sdl # no window manager at all +$ 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 +``` + +Either build also runs with no window manager at all: + +```shell +$ MADO_SCENE=none ./demo-sdl ``` -Each policy can be compiled out (`WM_DESKTOP`, `WM_MOBILE`) and switched at -run time with `twin_screen_set_wm()`. See [docs/phone.md](docs/phone.md) -for the design of the window manager, compositor and caches. +`twin_screen_set_wm()` switches between the built policy and the 'none' +policy at run time. Each demo app is registered in `apps/apps.c` with the +window managers it runs under: all three by default, the mobile one only +for the phone shell. See [docs/phone.md](docs/phone.md) for the design of +the window manager, compositor and caches. ### Verification @@ -145,7 +158,7 @@ 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 # core, window manager, lifetime (and phone) suites $ make check-update # re-record golden frame checksums ``` diff --git a/apps/animation.c b/apps/animation.c index f1e75bf6..b30a6bea 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -35,13 +35,23 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) current_frame = anim->pix; } + /* A policy may give the window more room than the image (a phone's + * work area, the whole screen without a window manager): center it */ + 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; + if (w > fw || h > fh) + twin_fill(dst, 0xff000000, TWIN_SOURCE, 0, 0, w, h); + 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, twin_pixmap_width(current_frame), - twin_pixmap_height(current_frame)); + 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) @@ -115,7 +125,7 @@ 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, +bool apps_animation_start(twin_screen_t *screen, const char *name, const char *path, int x, @@ -123,12 +133,17 @@ void apps_animation_start(twin_screen_t *screen, { twin_pixmap_t *pix = twin_pixmap_from_file(path, TWIN_ARGB32); if (!pix) - return; + return false; twin_toplevel_t *toplevel = twin_toplevel_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, twin_pixmap_width(pix), twin_pixmap_height(pix), name); - twin_custom_widget_t *anim = - apps_animation_create(twin_toplevel_box(toplevel), pix); - (void) anim; + 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_pixmap_destroy(pix); twin_toplevel_show(toplevel); + return true; } diff --git a/apps/apps.c b/apps/apps.c new file mode 100644 index 00000000..30f5264c --- /dev/null +++ b/apps/apps.c @@ -0,0 +1,121 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +#include "apps.h" + +#include "apps_animation.h" +#include "apps_calc.h" +#include "apps_clock.h" +#include "apps_image.h" +#include "apps_multi.h" +#include "apps_spline.h" + +#if defined(CONFIG_DEMO_CELLPHONE) +#include "phone.h" +#endif + +#define ASSET_PATH "assets/" + +#if defined(CONFIG_DEMO_CELLPHONE) +static bool start_phone(twin_screen_t *screen, int x, int y, int w, int h) +{ + (void) x, (void) y, (void) w, (void) h; + return phone_boot(screen); +} +#endif + +#if defined(CONFIG_DEMO_CLOCK) +static bool start_clock(twin_screen_t *screen, int x, int y, int w, int h) +{ + return apps_clock_start(screen, "Clock", x, y, w, h); +} +#endif + +#if defined(CONFIG_DEMO_CALCULATOR) +static bool start_calc(twin_screen_t *screen, int x, int y, int w, int h) +{ + return apps_calc_start(screen, "Calc", x, y, w, h); +} +#endif + +#if defined(CONFIG_DEMO_SPLINE) +static bool start_spline(twin_screen_t *screen, int x, int y, int w, int h) +{ + return apps_spline_start(screen, "Spline", x, y, w, h); +} +#endif + +#if defined(CONFIG_DEMO_ANIMATION) +/* Sized by the image */ +static bool start_animation(twin_screen_t *screen, int x, int y, int w, int h) +{ + (void) w, (void) h; + return apps_animation_start(screen, "Animation", ASSET_PATH "nyancat.gif", + x, y); +} +#endif + +#if defined(CONFIG_DEMO_IMAGE) +/* Fixed size */ +static bool start_image(twin_screen_t *screen, int x, int y, int w, int h) +{ + (void) w, (void) h; + return apps_image_start(screen, "Vectors", x, y); +} +#endif + +#if defined(CONFIG_DEMO_MULTI) +static bool start_multi(twin_screen_t *screen, int x, int y, int w, int h) +{ + return apps_multi_start(screen, "Demo", x, y, w, h); +} +#endif + +static const apps_entry_t apps[] = { +#if defined(CONFIG_DEMO_CELLPHONE) + /* The phone shell targets the mobile policy only; it lists the other + * apps that run there on its home screen */ + {"Phone", TWIN_SYM_MOBILE, APPS_WM(TwinWmMobile), true, 0, 0, PHONE_W, + PHONE_H, start_phone, phone_shutdown}, +#endif +#if defined(CONFIG_DEMO_CLOCK) + {"Clock", TWIN_SYM_CLOCK, APPS_WM_ALL, false, 10, 10, 200, 200, start_clock, + NULL}, +#endif +#if defined(CONFIG_DEMO_CALCULATOR) + {"Calc", TWIN_SYM_CALCULATOR, APPS_WM_ALL, false, 100, 100, 200, 200, + start_calc, NULL}, +#endif +#if defined(CONFIG_DEMO_SPLINE) + {"Spline", TWIN_SYM_SHUFFLE, APPS_WM_ALL, false, 20, 20, 400, 400, + start_spline, NULL}, +#endif +#if defined(CONFIG_DEMO_ANIMATION) + {"Animation", TWIN_SYM_PLAY, APPS_WM_ALL, false, 20, 20, 0, 0, + start_animation, NULL}, +#endif +#if defined(CONFIG_DEMO_IMAGE) + {"Vectors", TWIN_SYM_GLOBE, APPS_WM_ALL, false, 20, 20, 0, 0, start_image, + NULL}, +#endif +#if defined(CONFIG_DEMO_MULTI) + {"Demo", TWIN_SYM_LIST, APPS_WM_ALL, false, 100, 100, 400, 400, start_multi, + 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..232b3037 --- /dev/null +++ b/apps/apps.h @@ -0,0 +1,48 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Registry of the demo applications that are built, and of the window + * managers each one runs under. 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. + */ + +#ifndef _APPS_H_ +#define _APPS_H_ + +#include +#include + +/* Window managers an app runs under: one bit per twin_wm_mode_t. Apps + * that create ordinary windows run under every policy (APPS_WM_ALL). */ +#define APPS_WM(mode) (1u << (mode)) +#define APPS_WM_ALL \ + (APPS_WM(TwinWmNone) | APPS_WM(TwinWmDesktop) | APPS_WM(TwinWmMobile)) + +typedef struct apps_entry { + const char *name; /* window title and launcher label */ + const char *icon; /* TWIN_SYM_* shown by launchers */ + unsigned wm; /* APPS_WM() bits of the policies it runs under */ + /* Takes over the screen and launches the other apps itself (the phone + * shell): started alone, on a screen of its own w x h */ + bool shell; + int x, y, w, h; /* placement requested on a desktop */ + /* Open the app's windows on @screen at @x,@y, @w x @h (the policy may + * place them elsewhere); false when it could not start */ + bool (*start)(twin_screen_t *screen, int x, int y, int w, int h); + void (*stop)(void); /* tear down a shell; NULL for ordinary apps */ +} apps_entry_t; + +int apps_count(void); +const apps_entry_t *apps_at(int idx); + +static inline bool apps_runs_under(const apps_entry_t *app, twin_wm_mode_t mode) +{ + return app->wm & APPS_WM(mode); +} + +#endif /* _APPS_H_ */ diff --git a/apps/apps_animation.h b/apps/apps_animation.h index b6b88829..9363a24d 100644 --- a/apps/apps_animation.h +++ b/apps/apps_animation.h @@ -7,9 +7,10 @@ #ifndef _APPS_ANIMATION_H_ #define _APPS_ANIMATION_H_ +#include #include -void apps_animation_start(twin_screen_t *screen, +bool apps_animation_start(twin_screen_t *screen, const char *name, const char *path, int x, diff --git a/apps/apps_calc.h b/apps/apps_calc.h index 923de7da..a28de422 100644 --- a/apps/apps_calc.h +++ b/apps/apps_calc.h @@ -7,9 +7,10 @@ #ifndef _APPS_CALC_H_ #define _APPS_CALC_H_ +#include #include -void apps_calc_start(twin_screen_t *screen, +bool apps_calc_start(twin_screen_t *screen, const char *name, int x, int y, diff --git a/apps/apps_clock.h b/apps/apps_clock.h index 71518c8a..0ab84d52 100644 --- a/apps/apps_clock.h +++ b/apps/apps_clock.h @@ -7,9 +7,10 @@ #ifndef _APPS_CLOCK_H_ #define _APPS_CLOCK_H_ +#include #include -void apps_clock_start(twin_screen_t *screen, +bool apps_clock_start(twin_screen_t *screen, const char *name, int x, int y, diff --git a/apps/apps_image.h b/apps/apps_image.h index 06320d5f..1ea7d434 100644 --- a/apps/apps_image.h +++ b/apps/apps_image.h @@ -7,8 +7,9 @@ #ifndef _APPS_IMAGE_H_ #define _APPS_IMAGE_H_ +#include #include -void apps_image_start(twin_screen_t *screen, const char *name, int x, int y); +bool 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 index b4097b74..7598f904 100644 --- a/apps/apps_multi.h +++ b/apps/apps_multi.h @@ -7,9 +7,10 @@ #ifndef _APPS_MULTI_H_ #define _APPS_MULTI_H_ +#include #include -void apps_multi_start(twin_screen_t *screen, +bool apps_multi_start(twin_screen_t *screen, const char *name, int x, int y, diff --git a/apps/apps_spline.h b/apps/apps_spline.h index a527a20b..9f15add6 100644 --- a/apps/apps_spline.h +++ b/apps/apps_spline.h @@ -7,9 +7,10 @@ #ifndef _APPS_SPLINE_H_ #define _APPS_SPLINE_H_ +#include #include -void apps_spline_start(twin_screen_t *screen, +bool apps_spline_start(twin_screen_t *screen, const char *name, int x, int y, diff --git a/apps/calc.c b/apps/calc.c index f8be61e1..c00333e2 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -207,7 +207,7 @@ static twin_dispatch_result_t _apps_calc_display_event(twin_widget_t *widget, return TwinDispatchContinue; } -void apps_calc_start(twin_screen_t *screen, +bool apps_calc_start(twin_screen_t *screen, const char *name, int x, int y, @@ -215,9 +215,15 @@ 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); + if (!calc->toplevel) { + free(calc); + return false; + } twin_box_t *top = twin_toplevel_box(calc->toplevel); calc->display = twin_label_create(top, "0", APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, @@ -250,4 +256,5 @@ void apps_calc_start(twin_screen_t *screen, calc->pending_delete = true; calc->pending_op = 0; twin_toplevel_show(calc->toplevel); + return true; } diff --git a/apps/cellphone/games.c b/apps/cellphone/games.c index 89436c64..38843e89 100644 --- a/apps/cellphone/games.c +++ b/apps/cellphone/games.c @@ -211,10 +211,8 @@ 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. Only the mobile scene - * stacks views exclusively: desktop windows may sit side by side. */ - if (g->paused || (phone_top() != v && - twin_screen_get_wm(phone_screen()) != TwinWmDesktop)) + /* Freeze while another view covers this one */ + if (g->paused || phone_top() != v) return o->period; o->tick(g); g->steps++; diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index 99f8dbde..840a4136 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -630,9 +630,6 @@ ui_view_t *phone_top(void); /* topmost view, NULL if none */ void phone_lock(void); /* show the lock screen */ bool phone_locked(void); -/* Ask the status/nav bars to repaint (after navigation or theme change) */ -void phone_bars_refresh(void); - /* Home screen wallpaper */ int phone_wallpaper(void); int phone_wallpaper_count(void); @@ -641,9 +638,6 @@ 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); -/* Window manager scene (Settings): TwinWmMobile, TwinWmDesktop, TwinWmNone */ -bool phone_set_scene(twin_wm_mode_t mode); - /* Wall clock; tests install a fixed source */ typedef struct { int hour, min, sec; @@ -668,6 +662,9 @@ typedef struct { 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); diff --git a/apps/cellphone/settings.c b/apps/cellphone/settings.c index 04641133..9af9bbfc 100644 --- a/apps/cellphone/settings.c +++ b/apps/cellphone/settings.c @@ -31,7 +31,6 @@ #define INFO_H 30 #define CLOCK_H 86 #define PLATE 26 -#define BACK_W 40 #define TILE_GAP 12 #define TILE_LABEL_H 20 #define TILE_R 8 @@ -52,14 +51,12 @@ typedef enum { PAGE_DISPLAY, PAGE_SOUND, PAGE_WALLPAPER, - PAGE_WM, PAGE_TIME, PAGE_ABOUT, } page_id_t; static const char *const page_titles[] = { - "Settings", "Display", "Sound", "Wallpaper", - "Window manager", "Date & time", "About", + "Settings", "Display", "Sound", "Wallpaper", "Date & time", "About", }; typedef enum { @@ -74,13 +71,12 @@ typedef enum { IT_CLOCK, /* large time and date */ } item_kind_t; -enum { GROUP_THEME, GROUP_WM }; +enum { GROUP_THEME }; /* Info values; those from VAL_HEAP on are refreshed once per second */ enum { VAL_NAME, VAL_SCREEN, - VAL_WM, VAL_TZ, VAL_HEAP, VAL_PEAK, @@ -109,42 +105,17 @@ typedef struct { /* 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) */ - twin_wm_mode_t pending_wm; + 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 const char *wm_name(twin_wm_mode_t mode) -{ - switch (mode) { - case TwinWmDesktop: - return "Desktop"; - case TwinWmNone: - return "None"; - case TwinWmMobile: - default: - return "Mobile"; - } -} - -static twin_wm_mode_t current_wm(void) -{ - return phone_screen() ? twin_screen_get_wm(phone_screen()) : TwinWmMobile; -} - -/* Without the phone's nav bar there is no Back: offer one in the header */ -static bool has_back(void) -{ - return current_wm() != TwinWmMobile; -} - static int *pref_int(int id) { return id == PREF_BRIGHTNESS ? &prefs.brightness : &prefs.volume; @@ -202,8 +173,6 @@ static void format_value(int id, char *buf, size_t len) case VAL_SCREEN: snprintf(buf, len, "%dx%d", PHONE_W, PHONE_H); return; - case VAL_WM: - snprintf(buf, len, "%s", wm_name(current_wm())); return; case VAL_TZ: snprintf(buf, len, "GMT+8 Taipei"); @@ -289,8 +258,6 @@ static void build(page_t *s) add(s, IT_LINK, PAGE_WALLPAPER, 0, TWIN_SYM_IMAGE, UI_RGB(0xf97316), "Wallpaper", NULL); section(s, "System"); - add(s, IT_LINK, PAGE_WM, 0, TWIN_SYM_MOBILE, UI_RGB(0x8b5cf6), - "Window manager", NULL); add(s, IT_LINK, PAGE_TIME, 0, TWIN_SYM_CLOCK, UI_RGB(0x10b981), "Date & time", NULL); add(s, IT_LINK, PAGE_ABOUT, 0, TWIN_SYM_LIST, UI_RGB(0x64748b), "About", @@ -316,21 +283,6 @@ static void build(page_t *s) case PAGE_WALLPAPER: add(s, IT_WALLPAPERS, 0, 0, NULL, 0, "Wallpapers", NULL); break; - case PAGE_WM: - section(s, "Scene"); - add(s, IT_RADIO, GROUP_WM, TwinWmMobile, NULL, 0, "Mobile", - "App stack with phone bars"); -#if defined(CONFIG_WM_DESKTOP) - /* Only offer policies that were compiled in */ - add(s, IT_RADIO, GROUP_WM, TwinWmDesktop, NULL, 0, "Desktop", - "Overlapping, title bars"); -#endif - add(s, IT_RADIO, GROUP_WM, TwinWmNone, NULL, 0, "None", - "Every window full screen"); - add(s, IT_NOTE, 0, 0, NULL, 0, NULL, - "Desktop and None hide the status and nav bars. Come back here " - "to return to Mobile."); - break; case PAGE_TIME: add(s, IT_CLOCK, 0, 0, NULL, 0, "Clock", NULL); section(s, "Format"); @@ -344,7 +296,6 @@ static void build(page_t *s) section(s, "Device"); info(s, "Name", VAL_NAME); info(s, "Screen", VAL_SCREEN); - info(s, "Window manager", VAL_WM); section(s, "Memory"); info(s, "Heap in use", VAL_HEAP); info(s, "Heap peak", VAL_PEAK); @@ -475,12 +426,7 @@ static void paint_header(page_t *s, twin_pixmap_t *px) twin_coord_t w = ui_view_width(&s->view); ui_rect_t r = ui_rect(0, 0, w, HEADER_H); twin_coord_t x = 6; - /* Same look as ui_header(), with room for a back arrow */ ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 96)); - if (has_back()) { - ui_icon(px, TWIN_SYM_LEFT, FONT_NORMAL, 16, HEADER_H / 2, th->primary); - x = 28; - } ui_text_in(px, FONT_HEADING, ui_rect(x, 0, w - x - 8, HEADER_H), UI_LEFT, th->text, s->view.title); ui_separator(px, r); @@ -509,9 +455,7 @@ static void paint_swatch(twin_pixmap_t *px, twin_coord_t cy, int theme) static bool radio_on(const item_t *it) { - if (it->id == GROUP_THEME) - return phone_theme_index() == it->arg; - return (int) current_wm() == it->arg; + return it->id == GROUP_THEME && phone_theme_index() == it->arg; } static void link_detail(const item_t *it, char *buf, size_t len) @@ -527,9 +471,6 @@ static void link_detail(const item_t *it, char *buf, size_t len) case PAGE_WALLPAPER: snprintf(buf, len, "%s", phone_wallpaper_name(phone_wallpaper())); break; - case PAGE_WM: - snprintf(buf, len, "%s", wm_name(current_wm())); - break; default: break; } @@ -810,17 +751,6 @@ static void slide(page_t *s, twin_coord_t x) } } -/* Switching scenes re-lays out every window, this one included: do it - * from a timer once the current event has been fully dispatched. */ -static twin_time_t apply_scene(ui_view_t *v) -{ - page_t *s = (page_t *) v; - phone_set_scene(s->pending_wm); - refresh_others(s); - ui_view_invalidate_all(v); - return -1; -} - static void activate(page_t *s, int i, int tile) { item_t *it = &s->items[i]; @@ -831,12 +761,7 @@ static void activate(page_t *s, int i, int tile) case IT_RADIO: if (radio_on(it)) break; - if (it->id == GROUP_THEME) { - phone_theme_set(it->arg); /* repaints every window */ - } else { - s->pending_wm = (twin_wm_mode_t) it->arg; - ui_view_every(&s->view, 0, apply_scene); - } + phone_theme_set(it->arg); /* repaints every window */ break; case IT_SWITCH: { bool *b = pref_bool(it->id); @@ -909,10 +834,6 @@ static bool settings_event(ui_view_t *v, const ui_event_t *e) case UI_TAP: { int i = s->pressed, tile = s->pressed_tile; set_pressed(s, -1, -1); - if (e->y < HEADER_H && e->x < BACK_W && has_back()) { - ui_view_close(v); - return true; - } if (i >= 0 && i == item_at(s, e->x, e->y)) activate(s, i, tile); return true; diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index c4ab0173..c1a3ce9a 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -18,6 +18,7 @@ #include #include +#include "apps.h" #include "phone.h" /* @@ -144,12 +145,6 @@ static void _repaint_window(twin_window_t *w) twin_window_queue_paint(w); } -void phone_bars_refresh(void) -{ - _repaint_window(g_status); - _repaint_window(g_nav); -} - static int nav_state(void); /* The view stack changed: repaint the nav bar if its tabs look different */ @@ -276,36 +271,75 @@ 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(0x22c55e), app_dialer_open}, - {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x3b82f6), app_contacts_open}, - {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x10b981), app_sms_open}, - {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xf59e0b), app_calc_open}, - {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xec4899), app_music_open}, - {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xf97316), app_photos_open}, - {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6b7280), app_camera_open}, - {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x64748b), app_settings_open}, - {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x8b5cf6), app_calllog_open}, - {"Snake", NULL, glyph_snake, UI_RGB(0x14b8a6), app_snake_open}, - {"Pong", NULL, glyph_pong, UI_RGB(0x6366f1), app_pong_open}, - {"Tetris", NULL, glyph_tetris, UI_RGB(0xef4444), app_tetris_open}, + {"Phone", TWIN_SYM_PHONE, NULL, UI_RGB(0x22c55e), app_dialer_open, NULL}, + {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x3b82f6), app_contacts_open, + NULL}, + {"Messages", TWIN_SYM_MESSAGE, NULL, UI_RGB(0x10b981), app_sms_open, NULL}, + {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xf59e0b), app_calc_open, + NULL}, + {"Music", TWIN_SYM_MUSIC, NULL, UI_RGB(0xec4899), app_music_open, NULL}, + {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xf97316), app_photos_open, NULL}, + {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6b7280), app_camera_open, NULL}, + {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x64748b), app_settings_open, + NULL}, + {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x8b5cf6), app_calllog_open, + NULL}, + {"Snake", NULL, glyph_snake, UI_RGB(0x14b8a6), app_snake_open, NULL}, + {"Pong", NULL, glyph_pong, UI_RGB(0x6366f1), app_pong_open, NULL}, + {"Tetris", NULL, glyph_tetris, UI_RGB(0xef4444), app_tetris_open, NULL}, }; #define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) +/* Then every app from the shared registry that runs under the mobile + * policy (apps.c), in registry order */ +static phone_app_t demos[16]; +static int n_demos = -1; + +static void _list_demos(void) +{ + if (n_demos >= 0) + return; + static const twin_argb32_t accents[] = { + UI_RGB(0x0f766e), UI_RGB(0x78716c), UI_RGB(0xa855f7), + UI_RGB(0xd946ef), UI_RGB(0x84cc16), UI_RGB(0x0ea5e9), + }; + n_demos = 0; + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *e = apps_at(i); + if (e->shell || !apps_runs_under(e, TwinWmMobile)) + 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; + _list_demos(); + return N_APPS + n_demos; } const phone_app_t *phone_app_at(int idx) { - return (idx >= 0 && idx < N_APPS) ? &apps[idx] : NULL; + if (idx >= 0 && idx < N_APPS) + return &apps[idx]; + _list_demos(); + 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 < N_APPS; i++) - if (!strcmp(apps[i].name, name)) + for (int i = 0; i < phone_app_count(); i++) + if (!strcmp(phone_app_at(i)->name, name)) return i; return -1; } @@ -478,14 +512,14 @@ static int nav_hit(twin_coord_t x, twin_coord_t y) return -1; } -/* Destroy every app view except home and @keep, without animation */ -static void _drop_views(ui_view_t *keep) +static bool is_app_window(twin_window_t *w); + +/* 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); - ui_view_t *v = ui_view_of(w); - if (v && v != g_home && v != keep && - twin_window_style(w) == TwinWindowApplication) + if (w != keep && is_app_window(w) && (!g_home || w != g_home->window)) twin_window_destroy(w); w = next; } @@ -498,7 +532,7 @@ static void nav_go_root(const char *name) if (top && top->app == app) return; /* Tabs are destinations, not history: reset to home, then open */ - _drop_views(NULL); + _drop_apps(NULL); phone_open(app); } @@ -534,25 +568,31 @@ static bool nav_event(twin_window_t *w, twin_event_t *ev) /* * Navigation */ -static bool is_app_view(twin_window_t *w) +/* The stack holds phone views and the windows of registry apps alike */ +static bool is_app_window(twin_window_t *w) { - ui_view_t *v = ui_view_of(w); - return v && twin_window_is_mapped(w) && + return twin_window_is_mapped(w) && twin_window_style(w) == TwinWindowApplication; } -ui_view_t *phone_top(void) +static twin_window_t *_top_app(void) { - ui_view_t *top = NULL; + 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_view(w)) - top = ui_view_of(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; @@ -560,7 +600,7 @@ int phone_depth(void) return 0; for (twin_window_t *w = twin_screen_first_window(g_screen); w; w = twin_window_next(w)) - if (is_app_view(w) && ui_view_of(w) != g_home) + if (is_app_window(w) && (!g_home || w != g_home->window)) n++; return n; } @@ -568,7 +608,15 @@ int phone_depth(void) void phone_open(int app) { const phone_app_t *a = phone_app_at(app); - if (!a || !a->open) + if (!a) + return; + if (a->demo) { + /* Its own windows: the policy stacks them over the work area */ + a->demo->start(g_screen, 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 */ @@ -580,19 +628,19 @@ void phone_open(int app) void phone_back(void) { - ui_view_t *top = phone_top(); - if (top && top != g_home) - ui_view_close(top); + twin_window_t *top = _top_app(); + if (top && (!g_home || top != g_home->window)) + twin_window_back(top); } void phone_home(void) { - ui_view_t *top = phone_top(); - if (!top || top == g_home) + twin_window_t *top = _top_app(); + if (!top || (g_home && top == g_home->window)) return; /* Hidden screens go first (no animation), then the visible one pops */ - _drop_views(top); - ui_view_close(top); + _drop_apps(top); + twin_window_close(top); } static ui_view_t *lock_view(void) @@ -621,14 +669,6 @@ void phone_lock(void) ui_view_show(lock); } -bool phone_set_scene(twin_wm_mode_t mode) -{ - if (!g_screen || !twin_screen_set_wm(g_screen, mode)) - return false; - phone_bars_refresh(); - return true; -} - /* * Boot */ diff --git a/apps/cellphone/ui.c b/apps/cellphone/ui.c index e94ecea1..c699bfff 100644 --- a/apps/cellphone/ui.c +++ b/apps/cellphone/ui.c @@ -243,7 +243,7 @@ static bool _ui_event(twin_window_t *window, twin_event_t *ev) e.ucs4 = UI_KEY_BACKSPACE; return _deliver(v, &e); case TwinEventConfigure: - /* Sent only when the client size changed (a scene switch) */ + /* Sent only when the client size changed (the work area moved) */ ui_view_invalidate_all(v); if (v->layout) v->layout(v); @@ -254,6 +254,9 @@ static bool _ui_event(twin_window_t *window, twin_event_t *ev) 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 */ diff --git a/apps/clock.c b/apps/clock.c index b4fc0718..a583f6a0 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -252,7 +252,7 @@ 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, @@ -261,8 +261,11 @@ void apps_clock_start(twin_screen_t *screen, { twin_toplevel_t *toplevel = twin_toplevel_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + if (!toplevel) + return false; twin_custom_widget_t *clock = apps_clock_create(twin_toplevel_box(toplevel)); (void) clock; twin_toplevel_show(toplevel); + return true; } diff --git a/apps/image.c b/apps/image.c index 71585c04..744e4ceb 100644 --- a/apps/image.c +++ b/apps/image.c @@ -153,13 +153,16 @@ static twin_custom_widget_t *apps_image_create(twin_box_t *parent, return _apps_image_init(parent, height); } -void apps_image_start(twin_screen_t *screen, const char *name, int x, int y) +bool apps_image_start(twin_screen_t *screen, const char *name, int x, int y) { twin_toplevel_t *toplevel = twin_toplevel_create(screen, TWIN_ARGB32, TwinWindowApplication, x, y, APP_WIDTH, APP_HEIGHT, name); + if (!toplevel) + return false; twin_custom_widget_t *img = apps_image_create( twin_toplevel_box(toplevel), twin_screen_height(screen) * 3 / 4); (void) img; twin_toplevel_show(toplevel); + return true; } diff --git a/apps/main.c b/apps/main.c index 3d0428f2..8d619e6c 100644 --- a/apps/main.c +++ b/apps/main.c @@ -13,16 +13,7 @@ #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" - -#if defined(CONFIG_DEMO_CELLPHONE) -#include "phone.h" -#endif +#include "apps.h" #define WIDTH 640 #define HEIGHT 480 @@ -78,13 +69,13 @@ static twin_pixmap_t *load_background(twin_screen_t *screen, const char *path) static twin_context_t *tx = NULL; static twin_wm_mode_t scene; +static const apps_entry_t *shell; /* the shell the scene runs, if any */ static void cleanup(void) { -#if defined(CONFIG_DEMO_CELLPHONE) - if (scene == TwinWmMobile) - phone_shutdown(); -#endif + if (shell && shell->stop) + shell->stop(); + shell = NULL; twin_destroy(tx); tx = NULL; } @@ -98,20 +89,12 @@ static void sigint_handler(int sig) /* * Scene: which window manager the demo starts under. Kconfig picks the - * default; MADO_SCENE=mobile|desktop|none overrides it at run time. + * default; MADO_SCENE=mobile|desktop|none overrides it at run time when + * that window manager is built. */ -static twin_wm_mode_t pick_scene(void) +static twin_wm_mode_t default_scene(void) { - const char *env = getenv("MADO_SCENE"); - if (env) { - if (!strcmp(env, "mobile")) - return TwinWmMobile; - if (!strcmp(env, "desktop")) - return TwinWmDesktop; - if (!strcmp(env, "none")) - return TwinWmNone; - } #if defined(CONFIG_DEMO_SCENE_MOBILE) return TwinWmMobile; #elif defined(CONFIG_DEMO_SCENE_DESKTOP) @@ -121,73 +104,76 @@ static twin_wm_mode_t pick_scene(void) #endif } -static void init_desktop_apps(twin_screen_t *screen) +static twin_wm_mode_t pick_scene(void) { - (void) screen; -#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); + const char *env = getenv("MADO_SCENE"); + if (!env) + return default_scene(); +#if defined(CONFIG_WM_MOBILE) + if (!strcmp(env, "mobile")) + return TwinWmMobile; #endif -#if defined(CONFIG_DEMO_IMAGE) - apps_image_start(screen, "Viewer", 20, 20); +#if defined(CONFIG_WM_DESKTOP) + if (!strcmp(env, "desktop")) + return TwinWmDesktop; #endif + if (!strcmp(env, "none")) + return TwinWmNone; + fprintf(stderr, "MADO_SCENE=%s is not built, using the default scene\n", + env); + return default_scene(); } -/* Without a window manager every window covers the screen, so overlapping - * demos would hide each other: start only the first one. - */ -static void init_single_app(twin_screen_t *screen) +/* A shell that runs the whole scene (the phone under the mobile policy) */ +static const apps_entry_t *scene_shell(twin_wm_mode_t mode) { - (void) screen; -#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 + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *app = apps_at(i); + if (app->shell && apps_runs_under(app, mode)) + return app; + } + return NULL; } -/* Initialize demo applications based on build configuration. +/* Start the shell of the scene, or else every app that runs under it, + * last to first so that the first registry entries end up on top. + * 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) { twin_screen_t *screen = twin_context_screen(ctx); -#if defined(CONFIG_DEMO_CELLPHONE) - if (scene == TwinWmMobile) { - if (phone_boot(screen)) - return; - fprintf(stderr, - "cellphone: out of memory, showing the classic demos\n"); - } -#endif if (!twin_screen_set_wm(screen, scene)) scene = twin_screen_get_wm(screen); - if (scene == TwinWmDesktop) { + const apps_entry_t *sh = scene_shell(scene); + if (sh) { + if (sh->start(screen, 0, 0, sh->w, sh->h)) { + shell = sh; + return; + } + fprintf(stderr, "%s: out of memory, starting the apps instead\n", + sh->name); + } #if defined(CONFIG_WM_DESKTOP) + if (scene == TwinWmDesktop) twin_screen_set_background( screen, load_background(screen, ASSET_PATH "/tux.png")); #endif - init_desktop_apps(screen); - } else { - init_single_app(screen); + if (scene == TwinWmNone) { + for (int i = 0; i < apps_count(); i++) { + const apps_entry_t *app = apps_at(i); + if (!app->shell && apps_runs_under(app, scene) && + app->start(screen, app->x, app->y, app->w, app->h)) + break; + } + return; + } + for (int i = apps_count() - 1; i >= 0; i--) { + const apps_entry_t *app = apps_at(i); + if (!app->shell && apps_runs_under(app, scene)) + app->start(screen, app->x, app->y, app->w, app->h); } } @@ -196,15 +182,11 @@ int main(void) int width = WIDTH, height = HEIGHT; scene = pick_scene(); -#if defined(CONFIG_DEMO_CELLPHONE) - if (scene == TwinWmMobile) { - width = PHONE_W; - height = PHONE_H; + const apps_entry_t *sh = scene_shell(scene); + if (sh) { + width = sh->w; + height = sh->h; } -#else - if (scene == TwinWmMobile) - scene = TwinWmDesktop; -#endif tx = twin_create(width, height); if (!tx) diff --git a/apps/multi.c b/apps/multi.c index 32a4ca04..e415ce9a 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -107,7 +107,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); @@ -138,7 +138,7 @@ 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, @@ -146,6 +146,8 @@ static void apps_multiline_start(twin_screen_t *screen, { twin_toplevel_t *toplevel = twin_toplevel_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, "Lines"); + if (!toplevel) + return false; twin_custom_widget_t *custom = twin_custom_widget_create( twin_toplevel_box(toplevel), 0xffffffff, 0, 0, 1, 1, @@ -170,62 +172,97 @@ static void apps_multiline_start(twin_screen_t *screen, } twin_toplevel_show(toplevel); + return true; } -static void apps_circletext_start(twin_screen_t *screen, - int x, - int y, - int w, - int h) +/* + * 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 bool start_window(twin_screen_t *screen, + int x, + int y, + int w, + int h, + const char *name, + twin_draw_func_t draw) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_rect_t client = twin_window_client(window); - int wid = client.right - client.left; - int hei = client.bottom - client.top; - twin_pixmap_t *pixmap = twin_window_pixmap(window); - 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; + twin_window_set_name(window, name); + twin_window_set_callbacks(window, draw, NULL, NULL); + 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); + return true; +} - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, wid, hei); - twin_window_set_name(window, "circletext"); +/* Fill @path's coverage into the window in black over white */ +static void paint_text(twin_pixmap_t *pixmap, twin_path_t *path, int w, int h) +{ + twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); + twin_pixmap_t *alpha = twin_pixmap_create(TWIN_A8, w, h); + if (!alpha) + return; + twin_paint_path(alpha, 0xff000000, path); + twin_operand_t source = {.source_kind = TWIN_SOLID, .u.argb = 0xff000000}; + twin_operand_t mask = {.source_kind = TWIN_PIXMAP, .u.pixmap = alpha}; + twin_composite(pixmap, 0, 0, &source, 0, 0, &mask, 0, 0, TWIN_OVER, w, h); + twin_pixmap_destroy(alpha); +} +static void circletext_draw(twin_window_t *window) +{ + int w, h; + client_size(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); + paint_text(twin_window_pixmap(window), path, w, h); 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); + int w, h; + client_size(window, &w, &h); twin_pixmap_t *pixmap = twin_window_pixmap(window); - twin_window_set_name(window, "Smiley"); - /* Clear background to white */ twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); @@ -265,33 +302,17 @@ 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); - twin_rect_t client = twin_window_client(window); - int wid = client.right - client.left; - int hei = client.bottom - client.top; - twin_pixmap_t *pixmap = twin_window_pixmap(window); + int w, h; + client_size(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); @@ -304,18 +325,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); + paint_text(twin_window_pixmap(window), path, w, h); 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, @@ -345,29 +356,36 @@ 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); + int w, h; + client_size(window, &w, &h); twin_pixmap_t *pixmap = twin_window_pixmap(window); + 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); } /* @@ -418,53 +436,68 @@ static void box_blur(twin_pixmap_t *px, int r) free(tmp); } -static void apps_blur(twin_screen_t *screen, int x, int y, int w, int h) +/* 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 = NULL; -#if defined(CONFIG_LOADER_PNG) - raw_background = twin_pixmap_from_file(ASSET_PATH "tux.png", TWIN_ARGB32); -#endif - if (!raw_background) + 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_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - twin_window_set_name(window, "Blur"); - twin_rect_t client = twin_window_client(window); - int client_width = client.right - client.left; - int client_height = client.bottom - client.top; - twin_pixmap_t *scaled_background = - twin_pixmap_create(TWIN_ARGB32, client_width, client_height); 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(client_width)), + twin_int_to_fixed(w)), twin_fixed_div(twin_int_to_fixed(twin_pixmap_height(raw_background)), - twin_int_to_fixed(client_height))); + 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, - client_width, client_height); - - box_blur(scaled_background, 8); + 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; + srcop.u.pixmap = scaled; twin_composite(twin_window_pixmap(window), 0, 0, &srcop, 0, 0, NULL, 0, 0, - TWIN_SOURCE, client_width, client_height); + TWIN_SOURCE, w, h); + twin_pixmap_destroy(scaled); +} - twin_pixmap_destroy(scaled_background); - twin_pixmap_destroy(raw_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", TWIN_ARGB32); +#endif + if (!raw_background) + return false; + twin_window_t *window = twin_window_create( + screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); + if (!window) { + twin_pixmap_destroy(raw_background); + return false; + } + twin_window_set_name(window, "Blur"); + twin_window_set_client_data(window, raw_background); + twin_window_set_callbacks(window, blur_draw, NULL, blur_destroy); + 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); - return; + return true; } -void apps_multi_start(twin_screen_t *screen, +bool apps_multi_start(twin_screen_t *screen, const char *name, int x, int y, @@ -472,10 +505,13 @@ 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); + 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); + any |= start_window(screen, x += 20, y += 20, w, h, "ASCII", ascii_draw); + any |= start_window(screen, x += 20, y += 20, w, h, "Flower", flower_draw); + 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 3b8c09e3..b2013526 100644 --- a/apps/spline.c +++ b/apps/spline.c @@ -252,7 +252,7 @@ static twin_custom_widget_t *apps_spline_create(twin_box_t *parent, 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, @@ -261,8 +261,11 @@ void apps_spline_start(twin_screen_t *screen, { twin_toplevel_t *toplevel = twin_toplevel_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + if (!toplevel) + return false; twin_custom_widget_t *spline = apps_spline_create( twin_toplevel_box(toplevel), 4, twin_screen_height(screen) * 2 / 3); (void) spline; twin_toplevel_show(toplevel); + return true; } diff --git a/configs/Kconfig b/configs/Kconfig index f935da9d..29e3f698 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -227,11 +227,17 @@ config CURSOR menu "Window Management" -comment "The 'none' policy is always built: no decorations, no placement" +choice + prompt "Window manager" + default WM_DESKTOP + help + Select the window manager policy built into the library. Only one + can be built at a time; the 'none' policy (no decorations, no + placement) is always available besides it, and + twin_screen_set_wm() switches between the two at run time. config WM_DESKTOP bool "Desktop window manager" - default y help Overlapping windows with title bars, click-to-raise, drag-to-move, double-click maximize, edge snapping, corner resizing, @@ -242,7 +248,6 @@ config WM_DESKTOP config WM_MOBILE bool "Mobile window manager" - default y help Phone-style shell: application windows stack over the work area between optional system bars (status bar, navigation bar), only the @@ -251,6 +256,15 @@ config WM_MOBILE repainting applications. Escape also goes back. Selected at run time with twin_screen_set_wm(screen, TwinWmMobile). +config WM_NONE + bool "None" + help + Build only the 'none' policy: no decorations, every application + window covers the screen, and the keyboard goes to the newest or the + last pressed window. + +endchoice + config WM_MOBILE_EVICT bool "Release pixels of covered applications" default y @@ -263,7 +277,7 @@ config WM_MOBILE_EVICT config DROP_SHADOW bool "Window drop shadows" default y - depends on WM_DESKTOP || WM_MOBILE + depends on !WM_NONE help Cast a soft shadow under the active desktop window and under the sliding window of mobile transitions. The compositor synthesizes @@ -415,8 +429,9 @@ config DEMO_CELLPHONE Phone user interface: lock screen, home screen, dialer, contacts, messages, calculator, music, photos, camera, settings, call log and games, drawn with vector graphics on the mobile window manager. - Settings can switch the running system to the desktop or 'none' - window manager. + Every other app in apps/apps.c marked for the mobile policy (the + demos selected below) gets a launcher icon too and runs as a + stacked app; Back closes its windows one at a time. config DEMO_CELLPHONE_RGB16 bool "Use RGB565 surfaces for phone screens" @@ -430,15 +445,19 @@ config DEMO_CELLPHONE_RGB16 choice prompt "Demo scene at startup" depends on DEMO_APPLICATIONS - default DEMO_SCENE_MOBILE if DEMO_CELLPHONE + default DEMO_SCENE_MOBILE if WM_MOBILE default DEMO_SCENE_DESKTOP help - The demo launcher starts either the phone shell or the desktop - demos. MADO_SCENE=mobile|desktop|none overrides it at run time. + The window manager the demo launcher starts under. Each app in + apps/apps.c is marked with the window managers it runs under; the + launcher starts those marked for the scene, or the shell that owns + it (the phone under the mobile policy). 'No window manager' shows + the first app full screen. MADO_SCENE=mobile|desktop|none overrides + the choice at run time. config DEMO_SCENE_MOBILE - bool "Mobile (cellphone)" - depends on DEMO_CELLPHONE + bool "Mobile" + depends on WM_MOBILE config DEMO_SCENE_DESKTOP bool "Desktop" @@ -452,12 +471,12 @@ endchoice config DEMO_MULTI bool "Build multi demo" default y - depends on DEMO_APPLICATIONS && WM_DESKTOP && WIDGETS + 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" diff --git a/docs/backends.md b/docs/backends.md index 317ddb21..2b31e165 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -32,8 +32,8 @@ Run: ./demo-sdl ``` -Once the window appears, interact with the phone (the default scene) or, with -`MADO_SCENE=desktop`, move windows and interact with widgets. Screens up to +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, diff --git a/docs/phone.md b/docs/phone.md index eba5b8c7..4aff8ea2 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -1,6 +1,6 @@ # Mobile and Desktop Scenes -This document describes how Mado runs a phone user interface and a classic +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 @@ -20,8 +20,15 @@ at run time: | desktop | `TwinWmDesktop` | 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 | `TwinWmMobile` | 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 desktop and mobile policies are mutually exclusive: the Kconfig +"Window manager" choice builds one of them, or neither (`WM_NONE`), and +`twin_screen_set_wm()` switches between that one and none at run time. +Keeping one interactive policy per build keeps its code and state out of +targets that never use it, and a product ships as a phone or a desktop, +not both. + ```c -twin_screen_set_wm(screen, TwinWmMobile); /* switch at run time */ +twin_screen_set_wm(screen, TwinWmNone); /* switch at run time */ 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 */ @@ -35,8 +42,14 @@ 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 cellphone demo's Settings app switches between the -three while running. +manager at all. The demo apps are the proof. `apps/apps.c` registers +every app that is built together with the policies it runs under: +ordinary apps are marked for all three, and the phone shell, which owns the +screen, for the mobile policy only. The desktop scene opens the apps as +overlapping windows, the phone launcher lists every app marked for mobile +and the policy stacks its windows, and the 'none' scene shows one full +screen. `test-demos` runs each app under every policy it is marked for +that the build has. Switching policies re-lays out every existing window: decorations are added or removed, application windows are cascaded or stacked, and, with @@ -269,24 +282,32 @@ view die with it. 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. +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. -Run it with SDL (2x scaled by default; `MADO_SCALE=n` overrides): +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 && make -$ ./demo-sdl # phone (default scene) -$ MADO_SCENE=desktop ./demo-sdl # desktop demos +$ make defconfig +$ python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y +$ python3 tools/kconfig/genconfig.py configs/Kconfig +$ make +$ ./demo-sdl # phone (default scene of this build) $ MADO_SCENE=none ./demo-sdl # single app, no window manager ``` +The desktop demos are the default build's scene. + 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 four headless suites in-process: scan-out +`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. @@ -303,8 +324,11 @@ built-in renderer every frame is bit-exact across hosts. including mid-swipe, superseded and out-of-memory requests. - `test-lifetime`: callbacks that run while an object is torn down, or that act on the object calling them. -- `test-cellphone`: boots the phone with a fixed clock and drives the whole - user journey through every app. +- `test-demos`: every registry app under the built policy and 'none': + placement, consistency while it runs, closing window by window, 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: diff --git a/include/twin.h b/include/twin.h index 611f1f91..7e93e716 100644 --- a/include/twin.h +++ b/include/twin.h @@ -171,10 +171,11 @@ typedef struct _twin_event { * Window manager policy * * The window manager is a runtime-switchable policy layered over the - * compositor. Each policy can be compiled out (CONFIG_WM_DESKTOP, - * CONFIG_WM_MOBILE); TwinWmNone is always available: it adds no - * decorations, covers the screen with application windows, and gives the - * keyboard to the newest or the last pressed window. + * compositor. At most one of the desktop and mobile policies is built + * (CONFIG_WM_DESKTOP or CONFIG_WM_MOBILE); twin_screen_set_wm() refuses the + * other. TwinWmNone is always available: it adds no decorations, covers + * the screen with application windows, and gives the keyboard to the + * newest or the last pressed window. */ typedef enum _twin_wm_mode { TwinWmNone /**< No window management: windows map as requested */, diff --git a/src/twin_private.h b/src/twin_private.h index 39e8be37..db0fe457 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1188,6 +1188,10 @@ typedef struct _twin_wm_ops { void (*work_area)(const twin_screen_t *screen, twin_rect_t *area); } twin_wm_ops_t; +#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) +#error "CONFIG_WM_DESKTOP and CONFIG_WM_MOBILE are mutually exclusive" +#endif + extern const twin_wm_ops_t _twin_wm_none; #if defined(CONFIG_WM_DESKTOP) extern const twin_wm_ops_t _twin_wm_desktop; diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 9029a641..6855431d 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -1,6 +1,6 @@ # Golden frame checksums for the cellphone suite. # Regenerate with: make check-update -# config 2336132b +# config 2aae645b lock 052098fa home 5ede29ca calculator 749994d4 @@ -42,7 +42,7 @@ camera d68c4510 camera-shot 6e9386eb camera-video 63db382b camera-selfie 946f8ecd -settings a066cd81 +settings 131f4566 settings-display 58065c6c settings-sound 6b5df6cf settings-wallpaper 7a91c018 @@ -51,13 +51,10 @@ settings-display-dark 85a1f340 settings-home-dark e8e63c74 settings-sound-changed 54b18173 settings-wallpaper-dusk d9c420f3 -settings-wm 9ed9fd7e -settings-desktop 32891299 -settings-wm-mobile 9ed9fd7e -settings-none f60f298b games-paused 01480e92 games-snake 7d15230d games-pong d2b0caba games-tetris 31fd1cac games-tetris-late 4f253509 games-tetris-revealed 29ee7bdc +home-demos d7ac800b diff --git a/tests/golden/core.txt b/tests/golden/core.txt index b173571e..b37d3f22 100644 --- a/tests/golden/core.txt +++ b/tests/golden/core.txt @@ -1,4 +1,4 @@ # Golden frame checksums for the core suite. # Regenerate with: make check-update -# config 2336132b +# config 2aae645b scene 373a3045 diff --git a/tests/harness.c b/tests/harness.c index 0609d96c..ed4e2846 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -323,8 +323,7 @@ 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 " - "wm=%d", + "builtin=%d shadow=%d/%d/%d mont=%d icons=%d rgb16=%d jpeg=%d", #if defined(CONFIG_RENDERER_BUILTIN) 1, #else @@ -352,17 +351,9 @@ static uint32_t config_fingerprint(void) 0, #endif #if defined(CONFIG_LOADER_JPEG) - 1, + 1 #else - 0, -#endif - /* Screens list the policies that are built (Settings) */ 0 -#if defined(CONFIG_WM_DESKTOP) - | 1 -#endif -#if defined(CONFIG_WM_MOBILE) - | 2 #endif ); return th_crc_bytes(buf, strlen(buf)); diff --git a/tests/phone-demos.c b/tests/phone-demos.c new file mode 100644 index 00000000..53c7e6ca --- /dev/null +++ b/tests/phone-demos.c @@ -0,0 +1,134 @@ +/* + * 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" + +/* Home screen grid (home.c): 3x3 icons per page below a 6 px margin */ +#define COLS 3 +#define PER_PAGE 9 +#define CELL_W (PHONE_W / COLS) +#define CELL_H 74 +#define GRID_TOP 6 + +static uint32_t nav_crc(void) +{ + uint32_t crc = 0; + for (twin_coord_t y = PHONE_H - PHONE_NAV_H; y < PHONE_H; y++) + for (twin_coord_t x = 0; x < PHONE_W; x++) + crc = crc * 31 + th_pixel(phone_th, x, y); + return crc; +} + +static void tap_icon(int app) +{ + int k = app % PER_PAGE; + ph_tap((k % COLS) * CELL_W + CELL_W / 2, + GRID_TOP + (k / COLS) * CELL_H + CELL_H / 2 - 8); +} + +static twin_window_t *top_window(void) +{ + twin_window_t *top = NULL; + for (twin_window_t *w = phone_screen()->windows; w; w = w->wm_next) + if (w->mapped && w->style == TwinWindowApplication) + top = w; + return top; +} + +/* Swipe from the left edge across the screen: the mobile back gesture */ +static void edge_swipe(void) +{ + th_press(phone_th, 2, CY(120)); + for (twin_coord_t x = 30; x <= 210; x += 30) { + th_move(phone_th, x, CY(120)); + th_advance(phone_th, 16); + } + th_release(phone_th, 210, CY(120)); + th_settle(phone_th, 2000); +} + +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 */ + for (int p = 0; p < first / PER_PAGE; p++) + ph_drag(200, 120, 20, 120); + /* 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 (i / PER_PAGE != first / PER_PAGE) + break; /* the icons below are tested on this page only */ + tap_icon(i); + 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 = top_window(); + 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); + + /* The back gesture closes the window on top ... */ + int depth = phone_depth(); + edge_swipe(); + 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(); + + for (int p = 0; p < first / PER_PAGE; p++) + ph_drag(20, 120, 200, 120); +} diff --git a/tests/phone-scenes.c b/tests/phone-scenes.c index a4b06ffa..114d91bb 100644 --- a/tests/phone-scenes.c +++ b/tests/phone-scenes.c @@ -5,10 +5,10 @@ */ /* - * Every app under every scene: the same app code must fill whatever client - * area the 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. + * 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" @@ -48,31 +48,33 @@ static int unpainted(ui_view_t *v) return left; } -/* Switch the scene under the open app @v and check it re-laid out */ -static void check_scene(ui_view_t *v, - const char *name, - twin_wm_mode_t mode, - const char *scene, - twin_coord_t want_h) +/* 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) { - CHECK(phone_set_scene(mode), "%s under %s", name, scene); + twin_screen_set_inset(phone_screen(), inset); ph_wait(500); char what[64]; - snprintf(what, sizeof(what), "%s: %s view height", scene, name); - if (want_h) - CHECK_EQ(ui_view_height(v), want_h, what); + 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, "%s: %s left %d client pixels unpainted", scene, name, n); + CHECK(n == 0, "inset %d: %s left %d client pixels unpainted", inset, name, + n); } /* - * Open each app once and cycle the scenes underneath it. Every switch - * resizes the view, so this covers both laying out at creation (mobile) - * and re-laying out in place for each other scene. + * 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; const char *name = phone_app_at(i)->name; ui_view_t *v = ph_open(name); if (!v) @@ -80,14 +82,8 @@ void test_scenes(void) 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_scene(v, name, TwinWmNone, "none", PHONE_H); - check_scene(v, name, TwinWmMobile, "mobile after none", - PHONE_CONTENT_H); -#if defined(CONFIG_WM_DESKTOP) - check_scene(v, name, TwinWmDesktop, "desktop", 0); - check_scene(v, name, TwinWmMobile, "mobile after desktop", - PHONE_CONTENT_H); -#endif + 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 index fe70064c..74f94d4b 100644 --- a/tests/phone-settings.c +++ b/tests/phone-settings.c @@ -6,8 +6,7 @@ /* * Settings scenarios: every sub-screen opens and closes, theme, wallpaper, - * sliders and switches change state, the window manager scene switches to - * Desktop and None and back, and About refreshes only its values. + * sliders and switches change state, and About refreshes only its values. */ #include @@ -204,51 +203,6 @@ static void test_wallpaper(void) back_to_root(); } -static void test_window_manager(void) -{ - twin_screen_t *screen = phone_screen(); - if (!open_sub("Window manager")) - return; - ph_shot("settings-wm"); - -#if defined(CONFIG_WM_DESKTOP) - /* Desktop: views get title bars, the bars disappear */ - tap_item("Desktop"); - CHECK_EQ(twin_screen_get_wm(screen), TwinWmDesktop, "desktop scene"); - CHECK(top_is("Window manager"), "settings still on top in desktop"); - ph_shot("settings-desktop"); - tap_item("Mobile"); - CHECK_EQ(twin_screen_get_wm(screen), TwinWmMobile, "back to mobile"); - CHECK_EQ(phone_depth(), 2, "stack intact after desktop"); - CHECK_EQ(ui_view_height(phone_top()), PHONE_CONTENT_H, - "work-area height restored"); - ph_shot("settings-wm-mobile"); -#else - ui_rect_t none; - CHECK(!app_settings_item_rect("Desktop", &none), - "compiled-out desktop policy not offered"); -#endif - - /* None: every view covers the screen; the header offers Back */ - tap_item("None"); - CHECK_EQ(twin_screen_get_wm(screen), TwinWmNone, "none scene"); - CHECK_EQ(ui_view_height(phone_top()), PHONE_H, "view resized to screen"); - ph_shot("settings-none"); - view_tap(14, PHONE_HEADER_H / 2); - CHECK(top_is("Settings"), "header Back works without a nav bar"); - open_sub("Window manager"); - tap_item("Mobile"); - CHECK_EQ(twin_screen_get_wm(screen), TwinWmMobile, "mobile again"); - CHECK_EQ(ui_view_height(phone_top()), PHONE_CONTENT_H, - "view back in the work area"); - - /* The phone still navigates normally */ - back_to_root(); - if (open_sub("Display")) - back_to_root(); - CHECK_EQ(th_damage_check(phone_th), 0, "consistent after scene switches"); -} - static void test_about(void) { if (!open_sub("About")) @@ -283,7 +237,6 @@ void test_settings(void) test_display(); test_sound(); test_wallpaper(); - test_window_manager(); test_about(); ph_home(); diff --git a/tests/phone-test.h b/tests/phone-test.h index 35ad5158..da10a903 100644 --- a/tests/phone-test.h +++ b/tests/phone-test.h @@ -60,6 +60,7 @@ 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 index fc3afd6a..359b8fae 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -178,7 +178,7 @@ static void test_calculator(void) /* * Budgets for the whole phone: memory returns to baseline after visiting - * every app, the peak stays bounded, transitions cost about one frame of + * 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) @@ -186,6 +186,8 @@ 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(); @@ -200,6 +202,8 @@ static void test_budgets(void) 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); @@ -257,7 +261,7 @@ int main(void) void (*suites[])(void) = {test_dialer, test_calllog, test_contacts, test_sms, test_music, test_photos, test_camera, test_settings, test_games, - test_scenes, test_regress}; + test_scenes, test_demos, test_regress}; for (size_t i = 0; i < sizeof(suites) / sizeof(suites[0]); i++) { suites[i](); if (phone_depth()) diff --git a/tests/test-demos.c b/tests/test-demos.c new file mode 100644 index 00000000..1b10ddd7 --- /dev/null +++ b/tests/test-demos.c @@ -0,0 +1,154 @@ +/* + * Twin - A Tiny Window System + * Copyright (c) 2026 National Cheng Kung University, Taiwan + * All rights reserved. + */ + +/* + * Every app in the registry (apps.h) under each window manager it is marked + * for that this build has: the built policy (desktop or mobile) and 'none'. + * 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 BUILT_WM TwinWmMobile +#define W 240 +#define H 320 +#elif defined(CONFIG_WM_DESKTOP) +#define BUILT_WM TwinWmDesktop +#define W 640 +#define H 480 +#else +#define W 640 +#define H 480 +#endif + +/* Topmost shown application window, and how many there are */ +static twin_window_t *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; +} + +/* 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); + bool fills = t->screen->wm->mode != TwinWmDesktop; + 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 < t->width && p->y < t->height, + "%s: window on screen (%d,%d)", what, p->x, p->y); + } + } +} + +/* A window the policy just revealed was painted again: a released + * (evicted) window that nobody repainted comes back as one flat colour */ +static bool painted(th_t *t, const twin_window_t *w) +{ + twin_pixmap_t *p = w->pixmap; + twin_argb32_t first = th_pixel(t, p->x, p->y); + for (twin_coord_t y = p->y; y < p->y + p->height && y < t->height; y++) + for (twin_coord_t x = p->x; x < p->x + p->width && x < t->width; x++) + if (th_pixel(t, x, y) != first) + return true; + return false; +} + +static void run(const apps_entry_t *d, twin_wm_mode_t mode, const char *scene) +{ + char what[96]; + snprintf(what, sizeof(what), "%s under %s", d->name, scene); + + th_t *t = th_create(W, H, mode); + 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->x, d->y, d->w, d->h), "%s: started", what); + th_settle(t, 2000); + int n; + CHECK(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 = top_app(t->screen, NULL); + if (!top) + break; + if (mode == TwinWmMobile) + twin_window_back(top); + else + twin_window_close(top); + th_settle(t, 2000); + twin_window_t *next = top_app(t->screen, NULL); + if (next && next->pixmap && _twin_window_shown(next)) + CHECK(painted(t, next), "%s: revealed window painted", what); + } + CHECK(!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); +} + +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->shell) + continue; +#if defined(BUILT_WM) + if (apps_runs_under(d, BUILT_WM)) + run(d, BUILT_WM, BUILT_WM == TwinWmMobile ? "mobile" : "desktop"); +#endif + if (apps_runs_under(d, TwinWmNone)) + run(d, TwinWmNone, "none"); + } + twin_cache_flush(); + return th_report("demos"); +} diff --git a/tests/test-wm.c b/tests/test-wm.c index 852b601f..39ec7de9 100644 --- a/tests/test-wm.c +++ b/tests/test-wm.c @@ -17,6 +17,13 @@ #define W 240 #define H 320 + +/* The interactive policy this build has, if any; 'none' is always there */ +#if defined(CONFIG_WM_MOBILE) +#define BUILT_WM TwinWmMobile +#elif defined(CONFIG_WM_DESKTOP) +#define BUILT_WM TwinWmDesktop +#endif #define TOP_BAR 20 #define BOTTOM_BAR 32 @@ -654,7 +661,7 @@ static void test_switching(void) app_t extra = {.color = 0xffaaaa00}; twin_window_t *we = mk(t, &extra, TwinWindowApplication, 0, 0, W, H); th_advance(t, 48); - twin_screen_set_wm(t->screen, TwinWmDesktop); + twin_screen_set_wm(t->screen, TwinWmNone); th_settle(t, 1000); CONSISTENT(t, "switch: mid transition"); twin_screen_set_wm(t->screen, TwinWmMobile); @@ -675,7 +682,7 @@ static void test_switching(void) th_destroy(t); } -#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) +#if defined(BUILT_WM) static twin_wm_mode_t switch_to; static bool dispatch_result; @@ -692,38 +699,39 @@ static bool switching_event(twin_window_t *w, twin_event_t *ev) static void test_switch_from_handler(void) { -#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) - th_t *t = th_create(W, H, TwinWmMobile); +#if defined(BUILT_WM) + th_t *t = th_create(W, H, BUILT_WM); app_t a = {.color = 0xffaa3333}; twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); w->event = switching_event; th_settle(t, 1000); - switch_to = TwinWmDesktop; + switch_to = TwinWmNone; th_press(t, 100, 100); - CHECK(twin_screen_get_wm(t->screen) == TwinWmDesktop, + CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "pending switch reported at once"); th_release(t, 100, 100); th_settle(t, 1000); - CHECK(t->screen->wm->mode == TwinWmDesktop, - "switch applied after the event"); - CHECK(w->client.top > 0, "window decorated after the switch"); + CHECK(t->screen->wm->mode == TwinWmNone, "switch applied after the event"); + CHECK(w->client.top == 0 && w->pixmap->width == W, + "window full screen after the switch"); CONSISTENT(t, "switch from handler"); - switch_to = TwinWmMobile; + switch_to = BUILT_WM; th_tap(t, 100, 150); th_settle(t, 1000); - CHECK(t->screen->wm->mode == TwinWmMobile, "switched back from a handler"); + CHECK(t->screen->wm->mode == BUILT_WM, "switched back from a handler"); twin_window_destroy(w); - /* Phone-sized screens built for the work area cascade and stay on screen */ - twin_screen_set_wm(t->screen, TwinWmDesktop); +#if defined(CONFIG_WM_DESKTOP) + /* Phone-sized screens built for the work area cascade and stay on screen, + * also after a round trip through 'none' */ app_t apps[3] = { {.color = 0xff111111}, {.color = 0xff222222}, {.color = 0xff333333}}; twin_window_t *ws[3]; for (int i = 0; i < 3; i++) ws[i] = mk(t, &apps[i], TwinWindowApplication, 0, 20, W, 258); - twin_screen_set_wm(t->screen, TwinWmMobile); + twin_screen_set_wm(t->screen, TwinWmNone); twin_screen_set_wm(t->screen, TwinWmDesktop); th_settle(t, 1000); for (int i = 0; i < 3; i++) { @@ -737,6 +745,7 @@ static void test_switch_from_handler(void) CONSISTENT(t, "cascade on a small screen"); for (int i = 0; i < 3; i++) twin_window_destroy(ws[i]); +#endif th_destroy(t); #endif } @@ -1216,57 +1225,67 @@ static void test_mobile_bars(void) static void test_switch_pending(void) { -#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) - th_t *t = th_create(W, H, TwinWmMobile); +#if defined(BUILT_WM) + th_t *t = th_create(W, H, BUILT_WM); app_t a = {.color = 0xffaa3333}; twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); w->event = switching_event; th_settle(t, 1000); /* A direct request replaces one queued by a handler */ - switch_to = TwinWmDesktop; + switch_to = TwinWmNone; twin_event_t ev = {.kind = TwinEventButtonDown}; ev.u.pointer.screen_x = 100; ev.u.pointer.screen_y = 100; twin_screen_dispatch(t->screen, &ev); - CHECK(twin_screen_get_wm(t->screen) == TwinWmDesktop, "queued"); - CHECK(twin_screen_set_wm(t->screen, TwinWmNone), "direct switch"); + CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "queued"); + CHECK(twin_screen_set_wm(t->screen, BUILT_WM), "direct switch"); th_settle(t, 1000); - CHECK(t->screen->wm->mode == TwinWmNone, "stale queued switch dropped"); + CHECK(t->screen->wm->mode == BUILT_WM, "stale queued switch dropped"); th_release(t, 100, 100); + /* The policy that is not built is refused, from a handler too */ + switch_to = BUILT_WM == TwinWmMobile ? TwinWmDesktop : TwinWmMobile; + dispatch_result = true; + th_tap(t, 100, 100); + CHECK(!dispatch_result, "compiled-out policy refused from a handler"); + th_settle(t, 1000); + CHECK(t->screen->wm->mode == BUILT_WM, "current policy kept"); + +#if defined(CONFIG_WM_MOBILE) /* Switching while a swipe is under the finger */ - twin_screen_set_wm(t->screen, TwinWmMobile); w->event = app_event; app_t b = {.color = 0xff33aa33}; twin_window_t *wb = mk(t, &b, TwinWindowApplication, 0, 20, W, 258); th_settle(t, 1000); th_press(t, 3, 150); th_move(t, 60, 150); - twin_screen_set_wm(t->screen, TwinWmDesktop); + twin_screen_set_wm(t->screen, TwinWmNone); th_move(t, 90, 150); th_release(t, 90, 150); th_settle(t, 1000); CONSISTENT(t, "switch mid swipe"); twin_window_destroy(wb); + twin_screen_set_wm(t->screen, BUILT_WM); + th_settle(t, 1000); +#endif #if defined(CONFIG_MEMORY_STATS) /* Out of memory: a handler's switch is refused, not forced */ - twin_screen_set_wm(t->screen, TwinWmMobile); - th_settle(t, 1000); w->event = switching_event; - switch_to = TwinWmDesktop; + switch_to = TwinWmNone; _twin_alloc_fail_next = 1; dispatch_result = true; th_tap(t, 100, 100); _twin_alloc_fail_next = 0; CHECK(!dispatch_result, "queuing failure reported"); th_settle(t, 1000); - CHECK(t->screen->wm->mode == TwinWmMobile, "current policy kept"); + CHECK(t->screen->wm->mode == BUILT_WM, "current policy kept"); /* A policy that cannot start falls back to none */ + twin_screen_set_wm(t->screen, TwinWmNone); _twin_alloc_fail_next = 1; - CHECK(!twin_screen_set_wm(t->screen, TwinWmDesktop), "failed start"); + CHECK(!twin_screen_set_wm(t->screen, BUILT_WM), "failed start"); _twin_alloc_fail_next = 0; CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "fell back to none"); th_settle(t, 1000); From 6a56fbf9de0980cff077cc81ce0328b52f4bbbaf Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 20:36:41 +0800 Subject: [PATCH 09/48] Advance animation frames only on its timer The animation demo advanced a frame every time it painted, so any extra repaint (an exposure, a restore after eviction, the toplevel now repainting every widget on a window draw) skipped a frame and made the frame on screen depend on paint timing. The timer advances the frame and queues a paint; painting draws the current frame. --- apps/animation.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index b30a6bea..c6bbd677 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -27,13 +27,10 @@ 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; + /* 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); - if (a) { - current_frame = twin_animation_get_current_frame(a); - twin_animation_advance_frame(a); - } else { - current_frame = 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 */ @@ -60,8 +57,10 @@ 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); - return twin_animation_get_current_delay(twin_pixmap_animation(anim->pix)); + return twin_animation_get_current_delay(a); } static twin_dispatch_result_t _apps_animation_dispatch(twin_widget_t *widget, From fd6fa6330d719108b092e503916ae2412999a6c6 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 20:36:41 +0800 Subject: [PATCH 10/48] Start desktop apps in their former order The registry listed the multi-window demo last, which put its windows under the others but left the real-time clock on top, so the headless determinism check saw the second hand move between runs. List it first and start the apps in registry order, which restores the earlier desktop stacking exactly. The 'none' scene still starts the first app, now the multi-window demo, and the phone launcher lists it first. --- apps/apps.c | 8 ++++---- apps/main.c | 9 ++++----- tests/golden/cellphone.txt | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/apps.c b/apps/apps.c index 30f5264c..b930ffb0 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -81,6 +81,10 @@ static const apps_entry_t apps[] = { {"Phone", TWIN_SYM_MOBILE, APPS_WM(TwinWmMobile), true, 0, 0, PHONE_W, PHONE_H, start_phone, phone_shutdown}, #endif +#if defined(CONFIG_DEMO_MULTI) + {"Demo", TWIN_SYM_LIST, APPS_WM_ALL, false, 100, 100, 400, 400, start_multi, + NULL}, +#endif #if defined(CONFIG_DEMO_CLOCK) {"Clock", TWIN_SYM_CLOCK, APPS_WM_ALL, false, 10, 10, 200, 200, start_clock, NULL}, @@ -100,10 +104,6 @@ static const apps_entry_t apps[] = { #if defined(CONFIG_DEMO_IMAGE) {"Vectors", TWIN_SYM_GLOBE, APPS_WM_ALL, false, 20, 20, 0, 0, start_image, NULL}, -#endif -#if defined(CONFIG_DEMO_MULTI) - {"Demo", TWIN_SYM_LIST, APPS_WM_ALL, false, 100, 100, 400, 400, start_multi, - NULL}, #endif {0}, /* keeps the table valid when nothing is built */ }; diff --git a/apps/main.c b/apps/main.c index 8d619e6c..2e232772 100644 --- a/apps/main.c +++ b/apps/main.c @@ -135,10 +135,9 @@ static const apps_entry_t *scene_shell(twin_wm_mode_t mode) return NULL; } -/* Start the shell of the scene, or else every app that runs under it, - * last to first so that the first registry entries end up on top. - * Without a window manager every window covers the screen, so overlapping - * apps would hide each other: start only the first one. +/* Start the shell of the scene, or else every app that runs under it 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) @@ -170,7 +169,7 @@ static void init_demo_apps(twin_context_t *ctx) } return; } - for (int i = apps_count() - 1; i >= 0; i--) { + for (int i = 0; i < apps_count(); i++) { const apps_entry_t *app = apps_at(i); if (!app->shell && apps_runs_under(app, scene)) app->start(screen, app->x, app->y, app->w, app->h); diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 6855431d..5a30d44b 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -57,4 +57,4 @@ games-pong d2b0caba games-tetris 31fd1cac games-tetris-late 4f253509 games-tetris-revealed 29ee7bdc -home-demos d7ac800b +home-demos 622830ba From 979c232ba8a37f750b38c455118bf2a4d65463bf Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:03:30 +0800 Subject: [PATCH 11/48] Convert negative sfixed values without shifts twin_sfixed_to_fixed() shifted a possibly negative value left, which is undefined; text painted at a negative origin, as when a phone window shrinks under an on-screen keyboard, tripped UBSan. Multiply instead, as twin_fixed_to_xfixed() already does. --- src/twin_private.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/twin_private.h b/src/twin_private.h index db0fe457..07c16b3f 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -593,7 +593,7 @@ typedef int64_t twin_xfixed_t; #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)) From 49ab6087db63eb05b0454824ac4b16d27a88ec8a Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:03:30 +0800 Subject: [PATCH 12/48] Fail demo starts cleanly on widget failure A demo whose custom widget, label, box or button failed to allocate still showed an empty window and reported success. Each start now tears the toplevel down and returns false, with the new twin_toplevel_destroy(). twin_widget_set_callback() reports whether the callback was installed, so the calculator knows whether its display took ownership of its state and frees it exactly once. test-demos also starts every app with its first allocation failing and checks that nothing is left behind, and on a desktop requires each window to lie fully on screen, not only its origin. --- apps/animation.c | 5 ++++- apps/calc.c | 23 +++++++++++++++++++++-- apps/clock.c | 7 ++++--- apps/image.c | 10 +++++++--- apps/multi.c | 34 ++++++++++++++++++---------------- apps/spline.c | 8 +++++--- include/twin.h | 6 +++++- src/toplevel.c | 7 +++++++ src/widget.c | 7 ++++--- tests/test-demos.c | 35 +++++++++++++++++++++++++++++++++-- 10 files changed, 108 insertions(+), 34 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index c6bbd677..abe3829c 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -141,8 +141,11 @@ bool apps_animation_start(twin_screen_t *screen, return false; } /* The widget owns the image from here on */ - if (!apps_animation_create(twin_toplevel_box(toplevel), pix)) + 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/calc.c b/apps/calc.c index c00333e2..61b08680 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -225,25 +225,38 @@ bool apps_calc_start(twin_screen_t *screen, return false; } twin_box_t *top = twin_toplevel_box(calc->toplevel); + /* The display frees the calculator when the window goes away */ + bool owned = false; calc->display = twin_label_create(top, "0", APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, APPS_CALC_VALUE_STYLE); + if (!calc->display) + goto fail; twin_widget_t *display = twin_label_widget(calc->display); + owned = twin_widget_set_callback(display, _apps_calc_display_event, calc); + if (!owned) + goto fail; twin_widget_set(display, APPS_CALC_VALUE_BG); - twin_widget_set_callback(display, _apps_calc_display_event, calc); twin_label_set_align(calc->display, TwinAlignRight); twin_widget_set_shape(display, TwinShapeLozenge); calc->keys = twin_box_create(top, TwinBoxHorz); + if (!calc->keys) + goto fail; for (int i = 0; i < APPS_CALC_COLS; i++) { calc->cols[i] = twin_box_create(calc->keys, TwinBoxVert); + if (!calc->cols[i]) + goto fail; for (int j = 0; j < APPS_CALC_ROWS; j++) { int b = calc_layout[j][i]; calc->buttons[b] = twin_button_create( calc->cols[i], apps_calc_labels[b], APPS_CALC_BUTTON_FG, APPS_CALC_BUTTON_SIZE, APPS_CALC_BUTTON_STYLE); + if (!calc->buttons[b]) + goto fail; twin_widget_t *key = twin_button_widget(calc->buttons[b]); twin_widget_set(key, APPS_CALC_BUTTON_BG); - twin_widget_set_callback(key, _apps_calc_button_clicked, calc); + if (!twin_widget_set_callback(key, _apps_calc_button_clicked, calc)) + goto fail; twin_widget_set_shape(key, TwinShapeEllipse); if (i || j) twin_widget_match_size( @@ -257,4 +270,10 @@ bool apps_calc_start(twin_screen_t *screen, calc->pending_op = 0; twin_toplevel_show(calc->toplevel); return true; + +fail: + twin_toplevel_destroy(calc->toplevel); + if (!owned) + free(calc); + return false; } diff --git a/apps/clock.c b/apps/clock.c index a583f6a0..869a4839 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -263,9 +263,10 @@ bool apps_clock_start(twin_screen_t *screen, screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); if (!toplevel) return false; - twin_custom_widget_t *clock = - apps_clock_create(twin_toplevel_box(toplevel)); - (void) clock; + 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 744e4ceb..46b332eb 100644 --- a/apps/image.c +++ b/apps/image.c @@ -139,6 +139,8 @@ 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); + 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); @@ -160,9 +162,11 @@ bool apps_image_start(twin_screen_t *screen, const char *name, int x, int y) APP_WIDTH, APP_HEIGHT, name); if (!toplevel) return false; - twin_custom_widget_t *img = apps_image_create( - twin_toplevel_box(toplevel), twin_screen_height(screen) * 3 / 4); - (void) img; + 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/multi.c b/apps/multi.c index e415ce9a..293a418a 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -152,23 +152,25 @@ static bool apps_multiline_start(twin_screen_t *screen, twin_custom_widget_t *custom = twin_custom_widget_create( twin_toplevel_box(toplevel), 0xffffffff, 0, 0, 1, 1, _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); diff --git a/apps/spline.c b/apps/spline.c index b2013526..6663a0e3 100644 --- a/apps/spline.c +++ b/apps/spline.c @@ -263,9 +263,11 @@ bool apps_spline_start(twin_screen_t *screen, screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); if (!toplevel) return false; - twin_custom_widget_t *spline = apps_spline_create( - twin_toplevel_box(toplevel), 4, twin_screen_height(screen) * 2 / 3); - (void) spline; + 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/include/twin.h b/include/twin.h index 7e93e716..0d1e5368 100644 --- a/include/twin.h +++ b/include/twin.h @@ -482,8 +482,9 @@ 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); @@ -905,6 +906,9 @@ 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); diff --git a/src/toplevel.c b/src/toplevel.c index 26598e04..3da26c8d 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -184,6 +184,13 @@ void twin_toplevel_show(twin_toplevel_t *toplevel) twin_window_show(toplevel->box.widget.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/widget.c b/src/widget.c index 55224a7e..4838fd06 100644 --- a/src/widget.c +++ b/src/widget.c @@ -453,15 +453,16 @@ twin_pixmap_t *twin_custom_widget_pixmap(twin_custom_widget_t *custom) 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; + return true; } diff --git a/tests/test-demos.c b/tests/test-demos.c index 1b10ddd7..afb67f5f 100644 --- a/tests/test-demos.c +++ b/tests/test-demos.c @@ -63,8 +63,10 @@ static void check_placement(th_t *t, const char *what) "%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 < t->width && p->y < t->height, - "%s: window on screen (%d,%d)", what, p->x, p->y); + 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); } } } @@ -135,6 +137,32 @@ static void run(const apps_entry_t *d, twin_wm_mode_t mode, const char *scene) 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, twin_wm_mode_t mode) +{ + th_t *t = th_create(W, H, mode); + th_settle(t, 500); + size_t base = th_heap_net(t); + _twin_alloc_fail_next = 1; + bool ok = d->start(t->screen, d->x, d->y, d->w, d->h); + _twin_alloc_fail_next = 0; + int n; + 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 = 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++) { @@ -148,6 +176,9 @@ int main(void) #endif if (apps_runs_under(d, TwinWmNone)) run(d, TwinWmNone, "none"); +#if defined(CONFIG_MEMORY_STATS) + run_out_of_memory(d, TwinWmNone); +#endif } twin_cache_flush(); return th_report("demos"); From 5ab02b9d6656f98d4957a70b81f36c7958b519f0 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:03:30 +0800 Subject: [PATCH 13/48] Close demo windows when the phone shuts down phone_shutdown() destroyed only phone views, so a demo opened from the launcher outlived the shell. Destroy every application window too. --- apps/cellphone/shell.c | 5 +++-- tests/test-cellphone.c | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index c1a3ce9a..4bc86438 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -744,12 +744,13 @@ void phone_shutdown(void) if (g_tick) twin_clear_timeout(g_tick); g_tick = NULL; - /* Views first (top-down), then the bars */ + /* 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)) + if (ui_view_of(w) || twin_window_style(w) == TwinWindowApplication) last = w; if (!last) break; diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c index 359b8fae..ea699a7c 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -269,7 +269,15 @@ int main(void) } 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"); From 2cbed9ceddd4f6f9c60c10f24b95a6464462a478 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:03:30 +0800 Subject: [PATCH 14/48] Test demos under keyboard inset, gate CI The phone suite now shrinks the work area under each demo opened from the launcher and checks that its window follows and repaints. Changes to the workflows themselves now run the build lanes. The README notes that Photos shows JPEG fixtures, and the test list says test-demos leaves the phone shell to test-cellphone. --- .github/workflows/main.yml | 1 + README.md | 5 +++-- docs/phone.md | 6 ++++-- tests/phone-demos.c | 25 +++++++++++++++++++++++++ tests/phone-scenes.c | 2 +- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24c91ed8..122e257a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,7 @@ jobs: with: files: | .ci/** + .github/workflows/** mk/** include/** src/** diff --git a/README.md b/README.md index 7d09ad57..14264601 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,9 @@ 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 drawn with vector graphics on the mobile window manager; -the launcher also lists every demo app that is built: +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 diff --git a/docs/phone.md b/docs/phone.md index 4aff8ea2..b840bb36 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -324,8 +324,10 @@ built-in renderer every frame is bit-exact across hosts. including mid-swipe, superseded and out-of-memory requests. - `test-lifetime`: callbacks that run while an object is torn down, or that act on the object calling them. -- `test-demos`: every registry app under the built policy and 'none': - placement, consistency while it runs, closing window by window, leaks. +- `test-demos`: every registry app except the phone shell (covered by + `test-cellphone`) under the built policy and 'none': 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. diff --git a/tests/phone-demos.c b/tests/phone-demos.c index 53c7e6ca..bc018183 100644 --- a/tests/phone-demos.c +++ b/tests/phone-demos.c @@ -37,6 +37,17 @@ static void tap_icon(int app) GRID_TOP + (k / COLS) * CELL_H + CELL_H / 2 - 8); } +/* Not one flat colour: the window was painted at its current size */ +static bool painted(const twin_pixmap_t *p) +{ + twin_argb32_t first = th_pixel(phone_th, p->x, p->y); + for (twin_coord_t y = p->y; y < p->y + p->height; y++) + for (twin_coord_t x = p->x; x < p->x + p->width; x++) + if (th_pixel(phone_th, x, y) != first) + return true; + return false; +} + static twin_window_t *top_window(void) { twin_window_t *top = NULL; @@ -103,6 +114,20 @@ void test_demos(void) 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 && painted(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(); edge_swipe(); diff --git a/tests/phone-scenes.c b/tests/phone-scenes.c index 114d91bb..f3a72541 100644 --- a/tests/phone-scenes.c +++ b/tests/phone-scenes.c @@ -74,7 +74,7 @@ void test_scenes(void) { for (int i = 0; i < phone_app_count(); i++) { if (phone_app_at(i)->demo) - continue; + 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) From 548ab10c968553ac12e7c077aa6f9e275e51f384 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:19:07 +0800 Subject: [PATCH 15/48] Test out-of-memory starts under each policy test-demos started every app out of memory under 'none' only, whatever the app was marked for. Run it alongside the normal pass, under each policy the app is marked for that the build has, which is also what the test list in docs/phone.md describes. --- tests/test-demos.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test-demos.c b/tests/test-demos.c index afb67f5f..9346f3be 100644 --- a/tests/test-demos.c +++ b/tests/test-demos.c @@ -170,15 +170,24 @@ int main(void) /* Shells own the screen and have suites of their own */ if (d->shell) continue; + /* Each policy the app is marked for that this build has */ + const twin_wm_mode_t modes[] = { #if defined(BUILT_WM) - if (apps_runs_under(d, BUILT_WM)) - run(d, BUILT_WM, BUILT_WM == TwinWmMobile ? "mobile" : "desktop"); + BUILT_WM, #endif - if (apps_runs_under(d, TwinWmNone)) - run(d, TwinWmNone, "none"); + TwinWmNone, + }; + for (size_t m = 0; m < sizeof(modes) / sizeof(modes[0]); m++) { + if (!apps_runs_under(d, modes[m])) + continue; + run(d, modes[m], + modes[m] == TwinWmMobile ? "mobile" + : modes[m] == TwinWmDesktop ? "desktop" + : "none"); #if defined(CONFIG_MEMORY_STATS) - run_out_of_memory(d, TwinWmNone); + run_out_of_memory(d, modes[m]); #endif + } } twin_cache_flush(); return th_report("demos"); From 0e00a68b62d0eb166b8beebaef1f484c8c6b6ef1 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 21:37:55 +0800 Subject: [PATCH 16/48] Fix the window manager at build time The Kconfig choice already builds one window manager, so nothing needs to switch or check it at run time. A screen enters the built policy when it is created, and creation fails if the policy cannot start; twin_screen_set_wm(), twin_screen_get_wm(), twin_wm_mode_t and the queued-switch machinery are gone. The 'none' policy stays in every build as what a screen falls back to while it is torn down. The demo launcher no longer picks a scene: MADO_SCENE and the "Demo scene at startup" choice are gone, and it starts the phone shell when one is built, otherwise every app, or only the first under WM_NONE. Which window managers an app runs under is settled at build time as well: apps run under all three, and the phone shell depends on WM_MOBILE in Kconfig and is compiled into the registry only there, so the registry needs no run-time markers. The tests follow: the harness creates screens under the built policy, test-wm drops the switching cases and runs the 'none' cases in WM_NONE builds, test-demos runs each app under the built policy, and the lifetime test that switched policy from a destroy callback now re-lays out every window from it instead. CI runs the sanitizers on the WM_NONE build too. --- .github/workflows/headless-test.yml | 6 +- .github/workflows/main.yml | 2 +- README.md | 14 +- apps/apps.c | 29 ++- apps/apps.h | 24 +-- apps/cellphone/shell.c | 11 +- apps/main.c | 90 +++------ configs/Kconfig | 47 +---- docs/phone.md | 80 ++++---- include/twin.h | 44 +---- src/screen.c | 22 +-- src/twin_private.h | 38 ++-- src/wm-desktop.c | 37 ---- src/wm-mobile.c | 4 - src/wm.c | 121 +----------- tests/harness.c | 3 +- tests/harness.h | 4 +- tests/phone-settings.c | 2 - tests/test-cellphone.c | 4 +- tests/test-core.c | 30 +-- tests/test-demos.c | 54 +++--- tests/test-lifetime.c | 29 +-- tests/test-wm.c | 278 ++-------------------------- 23 files changed, 214 insertions(+), 759 deletions(-) diff --git a/.github/workflows/headless-test.yml b/.github/workflows/headless-test.yml index 232504ff..89ccfcf1 100644 --- a/.github/workflows/headless-test.yml +++ b/.github/workflows/headless-test.yml @@ -40,7 +40,6 @@ jobs: - name: Run basic headless test run: | # Start demo in background, capture stderr for memory stats - export MADO_SCENE=desktop timeout 30s ./demo-headless 2>demo_stderr.log & DEMO_PID=$! echo "Started demo-headless with PID $DEMO_PID" @@ -82,7 +81,6 @@ jobs: - name: Test deterministic rendering run: | - export MADO_SCENE=desktop # Run twice and compare outputs for determinism echo "First run..." timeout 10s ./demo-headless & @@ -127,7 +125,7 @@ jobs: - name: Memory leak detection with Valgrind run: | - export MADO_SCENE=desktop DEBUGINFOD_URLS= + export DEBUGINFOD_URLS= # Run with Valgrind for leak detection timeout 15s valgrind \ --leak-check=full \ @@ -175,7 +173,7 @@ jobs: - name: Phone end-to-end run: | # Boot the phone shell, unlock, open Calculator, type, screenshot - MADO_SCENE=mobile timeout 30s ./demo-headless 2>phone_stderr.log & + timeout 30s ./demo-headless 2>phone_stderr.log & PID=$! sleep 2 ./headless-ctl shot phone_lock.png diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 122e257a..e153c833 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,7 +90,7 @@ jobs: sudo apt-get install -y libsdl2-dev libjpeg-dev libpng-dev - name: AddressSanitizer + UndefinedBehaviorSanitizer run: | - for wm in WM_DESKTOP WM_MOBILE; do + 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 diff --git a/README.md b/README.md index 14264601..890fd847 100644 --- a/README.md +++ b/README.md @@ -140,16 +140,10 @@ $ make $ ./demo-sdl # phone; drag from the left edge to go back ``` -Either build also runs with no window manager at all: - -```shell -$ MADO_SCENE=none ./demo-sdl -``` - -`twin_screen_set_wm()` switches between the built policy and the 'none' -policy at run time. Each demo app is registered in `apps/apps.c` with the -window managers it runs under: all three by default, the mobile one only -for the phone shell. See [docs/phone.md](docs/phone.md) for the design of +`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 diff --git a/apps/apps.c b/apps/apps.c index b930ffb0..bbfe15d4 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -74,36 +74,31 @@ static bool start_multi(twin_screen_t *screen, int x, int y, int w, int h) } #endif +/* Every app below the phone runs under all three window managers */ static const apps_entry_t apps[] = { -#if defined(CONFIG_DEMO_CELLPHONE) - /* The phone shell targets the mobile policy only; it lists the other - * apps that run there on its home screen */ - {"Phone", TWIN_SYM_MOBILE, APPS_WM(TwinWmMobile), true, 0, 0, PHONE_W, - PHONE_H, start_phone, phone_shutdown}, +#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, true, 0, 0, PHONE_W, PHONE_H, start_phone, + phone_shutdown}, #endif #if defined(CONFIG_DEMO_MULTI) - {"Demo", TWIN_SYM_LIST, APPS_WM_ALL, false, 100, 100, 400, 400, start_multi, - NULL}, + {"Demo", TWIN_SYM_LIST, false, 100, 100, 400, 400, start_multi, NULL}, #endif #if defined(CONFIG_DEMO_CLOCK) - {"Clock", TWIN_SYM_CLOCK, APPS_WM_ALL, false, 10, 10, 200, 200, start_clock, - NULL}, + {"Clock", TWIN_SYM_CLOCK, false, 10, 10, 200, 200, start_clock, NULL}, #endif #if defined(CONFIG_DEMO_CALCULATOR) - {"Calc", TWIN_SYM_CALCULATOR, APPS_WM_ALL, false, 100, 100, 200, 200, - start_calc, NULL}, + {"Calc", TWIN_SYM_CALCULATOR, false, 100, 100, 200, 200, start_calc, NULL}, #endif #if defined(CONFIG_DEMO_SPLINE) - {"Spline", TWIN_SYM_SHUFFLE, APPS_WM_ALL, false, 20, 20, 400, 400, - start_spline, NULL}, + {"Spline", TWIN_SYM_SHUFFLE, false, 20, 20, 400, 400, start_spline, NULL}, #endif #if defined(CONFIG_DEMO_ANIMATION) - {"Animation", TWIN_SYM_PLAY, APPS_WM_ALL, false, 20, 20, 0, 0, - start_animation, NULL}, + {"Animation", TWIN_SYM_PLAY, false, 20, 20, 0, 0, start_animation, NULL}, #endif #if defined(CONFIG_DEMO_IMAGE) - {"Vectors", TWIN_SYM_GLOBE, APPS_WM_ALL, false, 20, 20, 0, 0, start_image, - NULL}, + {"Vectors", TWIN_SYM_GLOBE, false, 20, 20, 0, 0, start_image, NULL}, #endif {0}, /* keeps the table valid when nothing is built */ }; diff --git a/apps/apps.h b/apps/apps.h index 232b3037..caad0f55 100644 --- a/apps/apps.h +++ b/apps/apps.h @@ -5,10 +5,14 @@ */ /* - * Registry of the demo applications that are built, and of the window - * managers each one runs under. The demo launcher (main.c), the phone's - * home screen and the tests all read it, so a new app is listed everywhere - * by adding one entry. + * Registry of the 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_ @@ -17,16 +21,9 @@ #include #include -/* Window managers an app runs under: one bit per twin_wm_mode_t. Apps - * that create ordinary windows run under every policy (APPS_WM_ALL). */ -#define APPS_WM(mode) (1u << (mode)) -#define APPS_WM_ALL \ - (APPS_WM(TwinWmNone) | APPS_WM(TwinWmDesktop) | APPS_WM(TwinWmMobile)) - typedef struct apps_entry { const char *name; /* window title and launcher label */ const char *icon; /* TWIN_SYM_* shown by launchers */ - unsigned wm; /* APPS_WM() bits of the policies it runs under */ /* Takes over the screen and launches the other apps itself (the phone * shell): started alone, on a screen of its own w x h */ bool shell; @@ -40,9 +37,4 @@ typedef struct apps_entry { int apps_count(void); const apps_entry_t *apps_at(int idx); -static inline bool apps_runs_under(const apps_entry_t *app, twin_wm_mode_t mode) -{ - return app->wm & APPS_WM(mode); -} - #endif /* _APPS_H_ */ diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 4bc86438..e83616d7 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -291,8 +291,8 @@ static const phone_app_t apps[] = { #define N_APPS ((int) (sizeof(apps) / sizeof(apps[0]))) -/* Then every app from the shared registry that runs under the mobile - * policy (apps.c), in registry order */ +/* Then every other app of the shared registry (apps.c), in registry order: + * a mobile build compiles in only apps that run under it */ static phone_app_t demos[16]; static int n_demos = -1; @@ -307,7 +307,7 @@ static void _list_demos(void) n_demos = 0; for (int i = 0; i < apps_count(); i++) { const apps_entry_t *e = apps_at(i); - if (e->shell || !apps_runs_under(e, TwinWmMobile)) + if (e->shell) continue; if (n_demos == (int) (sizeof(demos) / sizeof(demos[0]))) break; @@ -713,11 +713,6 @@ bool phone_boot(twin_screen_t *screen) if (!ui_init()) return false; g_screen = screen; - if (twin_screen_get_wm(screen) != TwinWmMobile && - !twin_screen_set_wm(screen, TwinWmMobile)) { - phone_shutdown(); - return false; - } 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); diff --git a/apps/main.c b/apps/main.c index 2e232772..7b596ea6 100644 --- a/apps/main.c +++ b/apps/main.c @@ -68,8 +68,7 @@ static twin_pixmap_t *load_background(twin_screen_t *screen, const char *path) static twin_context_t *tx = NULL; -static twin_wm_mode_t scene; -static const apps_entry_t *shell; /* the shell the scene runs, if any */ +static const apps_entry_t *shell; /* the running shell, if any */ static void cleanup(void) { @@ -87,66 +86,25 @@ static void sigint_handler(int sig) exit(1); } -/* - * Scene: which window manager the demo starts under. Kconfig picks the - * default; MADO_SCENE=mobile|desktop|none overrides it at run time when - * that window manager is built. - */ - -static twin_wm_mode_t default_scene(void) +/* A shell that owns the screen (the phone, in a mobile build) */ +static const apps_entry_t *find_shell(void) { -#if defined(CONFIG_DEMO_SCENE_MOBILE) - return TwinWmMobile; -#elif defined(CONFIG_DEMO_SCENE_DESKTOP) - return TwinWmDesktop; -#else - return TwinWmNone; -#endif -} - -static twin_wm_mode_t pick_scene(void) -{ - const char *env = getenv("MADO_SCENE"); - if (!env) - return default_scene(); -#if defined(CONFIG_WM_MOBILE) - if (!strcmp(env, "mobile")) - return TwinWmMobile; -#endif -#if defined(CONFIG_WM_DESKTOP) - if (!strcmp(env, "desktop")) - return TwinWmDesktop; -#endif - if (!strcmp(env, "none")) - return TwinWmNone; - fprintf(stderr, "MADO_SCENE=%s is not built, using the default scene\n", - env); - return default_scene(); -} - -/* A shell that runs the whole scene (the phone under the mobile policy) */ -static const apps_entry_t *scene_shell(twin_wm_mode_t mode) -{ - for (int i = 0; i < apps_count(); i++) { - const apps_entry_t *app = apps_at(i); - if (app->shell && apps_runs_under(app, mode)) - return app; - } + for (int i = 0; i < apps_count(); i++) + if (apps_at(i)->shell) + return apps_at(i); return NULL; } -/* Start the shell of the scene, or else every app that runs under it in - * registry order. Without a window manager every window covers the screen, - * so overlapping apps would hide each other: start only the first one. +/* 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) { twin_screen_t *screen = twin_context_screen(ctx); - if (!twin_screen_set_wm(screen, scene)) - scene = twin_screen_get_wm(screen); - const apps_entry_t *sh = scene_shell(scene); + const apps_entry_t *sh = find_shell(); if (sh) { if (sh->start(screen, 0, 0, sh->w, sh->h)) { shell = sh; @@ -156,23 +114,20 @@ static void init_demo_apps(twin_context_t *ctx) sh->name); } #if defined(CONFIG_WM_DESKTOP) - if (scene == TwinWmDesktop) - twin_screen_set_background( - screen, load_background(screen, ASSET_PATH "/tux.png")); + twin_screen_set_background(screen, + load_background(screen, ASSET_PATH "/tux.png")); #endif - if (scene == TwinWmNone) { - for (int i = 0; i < apps_count(); i++) { - const apps_entry_t *app = apps_at(i); - if (!app->shell && apps_runs_under(app, scene) && - app->start(screen, app->x, app->y, app->w, app->h)) - break; - } - return; - } for (int i = 0; i < apps_count(); i++) { const apps_entry_t *app = apps_at(i); - if (!app->shell && apps_runs_under(app, scene)) - app->start(screen, app->x, app->y, app->w, app->h); + if (app->shell) + continue; + bool started = app->start(screen, app->x, app->y, app->w, app->h); +#if defined(CONFIG_WM_NONE) + if (started) + break; +#else + (void) started; +#endif } } @@ -180,8 +135,7 @@ int main(void) { int width = WIDTH, height = HEIGHT; - scene = pick_scene(); - const apps_entry_t *sh = scene_shell(scene); + const apps_entry_t *sh = find_shell(); if (sh) { width = sh->w; height = sh->h; diff --git a/configs/Kconfig b/configs/Kconfig index 29e3f698..37621ab1 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -231,10 +231,8 @@ choice prompt "Window manager" default WM_DESKTOP help - Select the window manager policy built into the library. Only one - can be built at a time; the 'none' policy (no decorations, no - placement) is always available besides it, and - twin_screen_set_wm() switches between the two at run time. + Select the window manager built into the library. Every screen + runs it for its whole life: there is no switching at run time. config WM_DESKTOP bool "Desktop window manager" @@ -242,7 +240,6 @@ config WM_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. - Selected at run time with twin_screen_set_wm(screen, TwinWmDesktop). Costs ~3-4 KB .text on ARM Cortex-M class targets. @@ -254,14 +251,13 @@ config WM_MOBILE 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. - Selected at run time with twin_screen_set_wm(screen, TwinWmMobile). config WM_NONE bool "None" help - Build only the 'none' policy: no decorations, every application - window covers the screen, and the keyboard goes to the newest or the - last pressed window. + 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 @@ -428,10 +424,9 @@ config DEMO_CELLPHONE help Phone user interface: lock screen, home screen, dialer, contacts, messages, calculator, music, photos, camera, settings, call log and - games, drawn with vector graphics on the mobile window manager. - Every other app in apps/apps.c marked for the mobile policy (the - demos selected below) gets a launcher icon too and runs as a - stacked app; Back closes its windows one at a time. + 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" @@ -442,32 +437,6 @@ config DEMO_CELLPHONE_RGB16 surfaces. RGB565 has no alpha channel, so screens are always opaque, at the cost of some banding in gradients. -choice - prompt "Demo scene at startup" - depends on DEMO_APPLICATIONS - default DEMO_SCENE_MOBILE if WM_MOBILE - default DEMO_SCENE_DESKTOP - help - The window manager the demo launcher starts under. Each app in - apps/apps.c is marked with the window managers it runs under; the - launcher starts those marked for the scene, or the shell that owns - it (the phone under the mobile policy). 'No window manager' shows - the first app full screen. MADO_SCENE=mobile|desktop|none overrides - the choice at run time. - -config DEMO_SCENE_MOBILE - bool "Mobile" - depends on WM_MOBILE - -config DEMO_SCENE_DESKTOP - bool "Desktop" - depends on WM_DESKTOP - -config DEMO_SCENE_NONE - bool "No window manager" - -endchoice - config DEMO_MULTI bool "Build multi demo" default y diff --git a/docs/phone.md b/docs/phone.md index b840bb36..8509a1c5 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -11,24 +11,22 @@ without graphics acceleration. 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* selected -at run time: - -| Policy | `twin_wm_mode_t` | Behavior | Kconfig | -|--------|------------------|----------|---------| -| none | `TwinWmNone` | 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. | always built | -| desktop | `TwinWmDesktop` | 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 | `TwinWmMobile` | 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 desktop and mobile policies are mutually exclusive: the Kconfig -"Window manager" choice builds one of them, or neither (`WM_NONE`), and -`twin_screen_set_wm()` switches between that one and none at run time. -Keeping one interactive policy per build keeps its code and state out of -targets that never use it, and a product ships as a phone or a desktop, -not both. +what happens when it closes) belongs to a *window manager policy* chosen +when the library is built: + +| Policy | Behavior | Kconfig | +|--------|----------|---------| +| none | 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 | 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 | 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. ```c -twin_screen_set_wm(screen, TwinWmNone); /* switch at run time */ 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 */ @@ -43,23 +41,15 @@ geometry: `TwinWindowPlain` (placed exactly as requested, never decorated), (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 together with the policies it runs under: -ordinary apps are marked for all three, and the phone shell, which owns the -screen, for the mobile policy only. The desktop scene opens the apps as -overlapping windows, the phone launcher lists every app marked for mobile -and the policy stacks its windows, and the 'none' scene shows one full -screen. `test-demos` runs each app under every policy it is marked for -that the build has. - -Switching policies re-lays out every existing window: decorations are added -or removed, application windows are cascaded or stacked, and, with -`WM_MOBILE_EVICT`, pixels of windows the new policy keeps hidden are -released. A switch requested from -an event handler is applied once that event has been delivered; a later -request replaces it, and if it cannot be queued (out of memory) the call -fails and the current policy stays. `screen->windows` lists -every window bottom to top and is the stacking order for all policies, so a -switch preserves which window is on top. +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 (`twin_wm_ops_t` in `src/twin_private.h`) has twelve hooks: enter/leave, layout, map/unmap, decorate, dispatch, hit test, a @@ -88,9 +78,9 @@ 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 always present `none` policy -and the switching logic; `src/wm-desktop.c` and `src/wm-mobile.c` are the -two optional policies. +which policy is running. `src/wm.c` holds the screen-wide services and +the `none` policy, which a screen also falls back to while it is torn +down; `src/wm-desktop.c` and `src/wm-mobile.c` are the other two. 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 @@ -110,7 +100,7 @@ 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 policy switch) is +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 @@ -295,11 +285,11 @@ $ make defconfig $ python3 tools/kconfig/setconfig.py --kconfig configs/Kconfig WM_MOBILE=y $ python3 tools/kconfig/genconfig.py configs/Kconfig $ make -$ ./demo-sdl # phone (default scene of this build) -$ MADO_SCENE=none ./demo-sdl # single app, no window manager +$ ./demo-sdl # the phone ``` -The desktop demos are the default build's scene. +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 @@ -315,17 +305,17 @@ 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 each policy, desktop activation, +- `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, eviction memory, and run-time switching - including mid-swipe, superseded and out-of-memory requests. + 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 and 'none': placement, + `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 @@ -347,7 +337,7 @@ The checks are layered: 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 and policy switches; covered screens cost no pixels; per-tick + 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, diff --git a/include/twin.h b/include/twin.h index 0d1e5368..69e4533a 100644 --- a/include/twin.h +++ b/include/twin.h @@ -167,22 +167,6 @@ typedef struct _twin_event { } u; /**< Event-specific data union */ } twin_event_t; -/** - * Window manager policy - * - * The window manager is a runtime-switchable policy layered over the - * compositor. At most one of the desktop and mobile policies is built - * (CONFIG_WM_DESKTOP or CONFIG_WM_MOBILE); twin_screen_set_wm() refuses the - * other. TwinWmNone is always available: it adds no decorations, covers - * the screen with application windows, and gives the keyboard to the - * newest or the last pressed window. - */ -typedef enum _twin_wm_mode { - TwinWmNone /**< No window management: windows map as requested */, - TwinWmDesktop /**< Overlapping, decorated, draggable windows */, - TwinWmMobile /**< Full-screen app stack with transitions */, -} twin_wm_mode_t; - /** * Compositor counters * @@ -1149,27 +1133,15 @@ void twin_window_scroll(twin_window_t *window, * Window manager */ -/** - * Select the window manager policy - * @screen : Screen to manage - * @mode : Policy to activate - * - * Existing windows are re-placed under the new policy: decorations are - * added or removed, app windows are stacked or cascaded, and the pixels - * of windows the policy keeps hidden are released. - * - * Safe to call from an event handler: the switch is then applied once the - * event has been delivered (twin_screen_get_wm() already reports it), and - * a later request replaces it. - * - * Return false if @mode was compiled out, or if the switch could not be - * queued (out of memory); the current policy then stays. If the new - * policy cannot start (out of memory), the screen falls back to TwinWmNone - * and false is returned. +/* + * 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. */ -bool twin_screen_set_wm(twin_screen_t *screen, twin_wm_mode_t mode); - -twin_wm_mode_t twin_screen_get_wm(const twin_screen_t *screen); /** * Register system bars (status bar, navigation bar) diff --git a/src/screen.c b/src/screen.c index d6e0daa7..14cadd76 100644 --- a/src/screen.c +++ b/src/screen.c @@ -54,7 +54,6 @@ twin_screen_t *twin_screen_create(twin_coord_t width, screen->put_begin = put_begin; screen->put_span = put_span; screen->closure = closure; - screen->wm = &_twin_wm_none; #ifndef TWIN_SCRATCH_SIZE #define TWIN_SCRATCH_SIZE (16 * 1024) @@ -62,11 +61,13 @@ twin_screen_t *twin_screen_create(twin_coord_t width, screen->scratch_buf = twin_malloc(TWIN_SCRATCH_SIZE); screen->scratch_size = screen->scratch_buf ? TWIN_SCRATCH_SIZE : 0; -#if defined(CONFIG_WM_DESKTOP) - twin_screen_set_wm(screen, TwinWmDesktop); -#elif defined(CONFIG_WM_MOBILE) - twin_screen_set_wm(screen, TwinWmMobile); -#endif + /* The window manager the library was built with, for good */ + screen->wm = &_twin_wm_built; + if (screen->wm->enter && !screen->wm->enter(screen)) { + twin_free(screen->scratch_buf); + twin_free(screen); + return NULL; + } /* Nothing has been shown yet: the first update paints everything */ twin_screen_damage(screen, 0, 0, width, height); return screen; @@ -74,9 +75,8 @@ twin_screen_t *twin_screen_create(twin_coord_t width, void twin_screen_destroy(twin_screen_t *screen) { - _twin_screen_drop_pending_wm(screen); - - /* Let the policy finish animations and drop pixmaps it owns. */ + /* Let the policy finish animations and drop pixmaps it owns, then tear + * the windows down under 'none', which keeps no state */ if (screen->wm->leave) screen->wm->leave(screen); screen->wm = &_twin_wm_none; @@ -716,7 +716,7 @@ void twin_screen_set_active(twin_screen_t *screen, twin_pixmap_t *pixmap) if (old == pixmap) return; screen->active = pixmap; - /* Handlers run under the busy guard (a policy switch waits) */ + /* Handlers run under the busy guard */ _twin_screen_busy(screen); if (old) { ev.kind = TwinEventDeactivate; @@ -883,7 +883,7 @@ 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) { - /* Handlers may ask for a policy switch; it waits until we are out */ + /* 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); diff --git a/src/twin_private.h b/src/twin_private.h index 07c16b3f..92395eb3 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -217,11 +217,9 @@ struct _twin_screen { twin_window_t *windows; /**< Every window, bottom to top */ twin_window_t *bar_top; /**< System bar above the work area */ twin_window_t *bar_bottom; /**< System bar below the work area */ - const struct _twin_wm_ops *wm_pending; /**< Switch requested mid-event */ - struct _twin_work *wm_work; /**< Work item applying it */ - int dispatching; /**< Busy depth: dispatch, relayout, switch */ - twin_coord_t inset_bottom; /**< Reserved above the bottom bar (IME) */ - twin_window_t *zombies; /**< Destroyed while busy, freed when idle */ + int dispatching; /**< Busy depth: dispatch, relayout */ + twin_coord_t inset_bottom; /**< Reserved above the bottom bar (IME) */ + twin_window_t *zombies; /**< Destroyed while busy, freed when idle */ twin_screen_stats_t stats; /**< Compositor work counters */ @@ -1138,14 +1136,13 @@ typedef struct twin_backend { * A policy decides window geometry, decorations, stacking, and how pointer * events reach clients. window.c owns the window objects and calls into the * active policy at a handful of well-defined points; policies never free a - * window. The 'none' policy (wm.c) is always present. + * window. The build selects one policy (_twin_wm_built); the 'none' + * policy (wm.c) is always present. */ typedef struct _twin_wm_ops { - twin_wm_mode_t mode; - - /* Policy becomes active / inactive. enter() re-lays out and maps every - * window; returning false (e.g. out of memory for its state) makes the - * screen fall back to the 'none' policy. */ + /* Policy becomes active when the screen is created, and inactive when + * it is destroyed. enter() returns false when it cannot start (out of + * memory for its state); the screen is then not created. */ bool (*enter)(twin_screen_t *screen); void (*leave)(twin_screen_t *screen); @@ -1192,12 +1189,16 @@ typedef struct _twin_wm_ops { #error "CONFIG_WM_DESKTOP and CONFIG_WM_MOBILE are mutually exclusive" #endif +/* 'none' is also what a screen falls back to while it is torn down */ extern const twin_wm_ops_t _twin_wm_none; #if defined(CONFIG_WM_DESKTOP) extern const twin_wm_ops_t _twin_wm_desktop; -#endif -#if defined(CONFIG_WM_MOBILE) +#define _twin_wm_built _twin_wm_desktop +#elif defined(CONFIG_WM_MOBILE) extern const twin_wm_ops_t _twin_wm_mobile; +#define _twin_wm_built _twin_wm_mobile +#else +#define _twin_wm_built _twin_wm_none #endif /* window.c services used by policies */ @@ -1212,10 +1213,10 @@ bool _twin_window_shown(const twin_window_t *window); 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 - * policy switch). 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. + * 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); @@ -1224,9 +1225,6 @@ void _twin_screen_idle(twin_screen_t *screen); * skipping the policy's gesture filter */ bool _twin_screen_route(twin_screen_t *screen, twin_event_t *event); -/* wm.c: forget a policy switch queued from an event handler */ -void _twin_screen_drop_pending_wm(twin_screen_t *screen); - /* 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); diff --git a/src/wm-desktop.c b/src/wm-desktop.c index 237849ee..6e857a14 100644 --- a/src/wm-desktop.c +++ b/src/wm-desktop.c @@ -658,33 +658,6 @@ static bool desktop_enter(twin_screen_t *screen) if (!d) return false; screen->wm_data = d; - - /* Place every application window afresh, bottom to top (leave cleared - * wm_placed) */ - for (twin_window_t *w = screen->windows; w; w = w->wm_next) { - _place_new(w); - _twin_window_relayout(w); - } - /* Map in stacking order; desktop_map raises, so iterate a snapshot. */ - twin_window_t *top = NULL; - int n = 0; - for (twin_window_t *w = screen->windows; w; w = w->wm_next) - n++; - for (int i = 0; i < n; i++) { - twin_window_t *w = screen->windows; - /* Activation handlers may have destroyed windows meanwhile */ - if (!w) - break; - if (w->mapped && !_twin_window_is_bar(w)) { - desktop_map(w); - if (w->style != TwinWindowPlain) - top = w; - } else { - _twin_window_raise(w); - } - } - if (top) - _activate(top); return true; } @@ -720,21 +693,11 @@ static void desktop_leave(twin_screen_t *screen) twin_desktop_t *d = DESKTOP(screen); if (d->resize_work) twin_clear_work(d->resize_work); - for (twin_window_t *w = screen->windows; w; w = w->wm_next) { - w->active = false; - w->client_grab = false; - /* Placement is redone on the next entry; maximized stays as is */ - w->wm_placed = false; - /* A window being destroyed may already have lost its pixmap */ - if (w->pixmap) - twin_pixmap_set_shadow(w->pixmap, 0, 0, 0, 0); - } twin_free(screen->wm_data); screen->wm_data = NULL; } const twin_wm_ops_t _twin_wm_desktop = { - .mode = TwinWmDesktop, .enter = desktop_enter, .leave = desktop_leave, .layout = desktop_layout, diff --git a/src/wm-mobile.c b/src/wm-mobile.c index d1290bad..a84f80a6 100644 --- a/src/wm-mobile.c +++ b/src/wm-mobile.c @@ -572,9 +572,6 @@ static bool mobile_enter(twin_screen_t *screen) return false; screen->wm_data = m; screen->background_color = 0xff000000; - for (twin_window_t *w = screen->windows; w; w = w->wm_next) - _twin_window_relayout(w); - mobile_settle(screen); return true; } @@ -615,7 +612,6 @@ static void mobile_work_area(const twin_screen_t *screen, twin_rect_t *area) } const twin_wm_ops_t _twin_wm_mobile = { - .mode = TwinWmMobile, .enter = mobile_enter, .leave = mobile_leave, .layout = mobile_layout, diff --git a/src/wm.c b/src/wm.c index f9aec481..d05c818c 100644 --- a/src/wm.c +++ b/src/wm.c @@ -5,9 +5,10 @@ */ /* - * Window manager selection and the 'none' policy + * Screen-wide window management services and the 'none' policy * - * The 'none' policy is the floor every build has: Plain windows map where + * The 'none' policy is what a WM_NONE build runs, and what every screen + * falls back to while it is torn down: 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 @@ -73,129 +74,13 @@ static bool none_dispatch(twin_window_t *window, twin_event_t *event) return _twin_window_deliver(window, event); } -static bool none_enter(twin_screen_t *screen) -{ - for (twin_window_t *w = screen->windows; w; w = w->wm_next) { - _twin_window_relayout(w); - if (w->mapped) - none_map(w); - } - return true; -} - const twin_wm_ops_t _twin_wm_none = { - .mode = TwinWmNone, - .enter = none_enter, .layout = none_layout, .map = none_map, .unmap = none_unmap, .dispatch = none_dispatch, }; -static const twin_wm_ops_t *_twin_wm_lookup(twin_wm_mode_t mode) -{ - switch (mode) { - case TwinWmNone: - return &_twin_wm_none; -#if defined(CONFIG_WM_DESKTOP) - case TwinWmDesktop: - return &_twin_wm_desktop; -#endif -#if defined(CONFIG_WM_MOBILE) - case TwinWmMobile: - return &_twin_wm_mobile; -#endif - default: - return NULL; - } -} - -static void _twin_screen_switch_wm(twin_screen_t *screen, - const twin_wm_ops_t *ops) -{ - if (screen->wm == ops) - return; - - _twin_screen_busy(screen); - twin_screen_disable_update(screen); - if (screen->wm->leave) - screen->wm->leave(screen); - - /* Neutral state: nothing shown, no policy-specific attributes. */ - 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; - twin_pixmap_hide(w->pixmap); - w->pixmap->scan_height = w->pixmap->height; /* not iconified */ - w->active = false; - w->pixmap->opacity = 0xff; - w->pixmap->shadow = (twin_shadow_t) {0}; - } - screen->background_color = 0xffffffff; - screen->active = NULL; - screen->target = NULL; - screen->clicklock = false; - - screen->wm = ops; - if (!ops->enter || !ops->enter(screen)) { - screen->wm = &_twin_wm_none; - none_enter(screen); - } - twin_screen_damage(screen, 0, 0, screen->width, screen->height); - twin_screen_enable_update(screen); - _twin_screen_idle(screen); -} - -void _twin_screen_drop_pending_wm(twin_screen_t *screen) -{ - if (screen->wm_work) - twin_clear_work(screen->wm_work); - screen->wm_work = NULL; - screen->wm_pending = NULL; -} - -static bool _twin_screen_apply_pending_wm(void *closure) -{ - twin_screen_t *screen = closure; - const twin_wm_ops_t *ops = screen->wm_pending; - screen->wm_pending = NULL; - screen->wm_work = NULL; - if (ops) - _twin_screen_switch_wm(screen, ops); - return false; -} - -bool twin_screen_set_wm(twin_screen_t *screen, twin_wm_mode_t mode) -{ - const twin_wm_ops_t *ops = _twin_wm_lookup(mode); - if (!ops) - return false; - - /* Switching re-lays out every window, including the one whose event - * is being delivered, and frees the old policy's state: never do that - * underneath a running dispatch. */ - if (screen->dispatching) { - if (!screen->wm_work) - screen->wm_work = twin_set_work(_twin_screen_apply_pending_wm, - TWIN_WORK_LAYOUT, screen); - /* Out of memory: refuse rather than switch under the dispatch */ - if (!screen->wm_work) - return false; - screen->wm_pending = ops == screen->wm ? NULL : ops; - return true; - } - /* A newer request replaces whatever a handler queued */ - _twin_screen_drop_pending_wm(screen); - _twin_screen_switch_wm(screen, ops); - return screen->wm == ops; -} - -twin_wm_mode_t twin_screen_get_wm(const twin_screen_t *screen) -{ - return screen->wm_pending ? screen->wm_pending->mode : screen->wm->mode; -} - void _twin_screen_relayout(twin_screen_t *screen) { /* Configure handlers run here: keep destroyed windows readable */ diff --git a/tests/harness.c b/tests/harness.c index ed4e2846..15100955 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -29,7 +29,7 @@ static void th_put_span(twin_coord_t left, sizeof(twin_argb32_t) * (size_t) (right - left)); } -th_t *th_create(twin_coord_t width, twin_coord_t height, twin_wm_mode_t wm) +th_t *th_create(twin_coord_t width, twin_coord_t height) { th_t *t = calloc(1, sizeof(*t)); if (!t) @@ -46,7 +46,6 @@ th_t *th_create(twin_coord_t width, twin_coord_t height, twin_wm_mode_t wm) free(t); return NULL; } - twin_screen_set_wm(t->screen, wm); th_frame(t); return t; } diff --git a/tests/harness.h b/tests/harness.h index 5051d4d3..a3a9a2e8 100644 --- a/tests/harness.h +++ b/tests/harness.h @@ -30,8 +30,8 @@ typedef struct { twin_coord_t width, height; } th_t; -/* Create a screen of the given size under the given window manager */ -th_t *th_create(twin_coord_t width, twin_coord_t height, twin_wm_mode_t wm); +/* 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 */ diff --git a/tests/phone-settings.c b/tests/phone-settings.c index 74f94d4b..8df7808c 100644 --- a/tests/phone-settings.c +++ b/tests/phone-settings.c @@ -242,6 +242,4 @@ void test_settings(void) ph_home(); CHECK_EQ(phone_theme_index(), 0, "theme 0 at exit"); CHECK_EQ(phone_wallpaper(), 0, "wallpaper 0 at exit"); - CHECK_EQ(twin_screen_get_wm(phone_screen()), TwinWmMobile, - "mobile scene at exit"); } diff --git a/tests/test-cellphone.c b/tests/test-cellphone.c index ea699a7c..abe26e8c 100644 --- a/tests/test-cellphone.c +++ b/tests/test-cellphone.c @@ -7,7 +7,7 @@ /* * 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, window-manager scene switch). + * paging, keyboard input, theme switch, apps opened from the launcher). * * Gates: * - structure: stack depth, top view, focus after each step @@ -249,7 +249,7 @@ static void test_budgets(void) int main(void) { phone_set_clock_source(fixed_clock); - t = phone_th = th_create(PHONE_W, PHONE_H, TwinWmMobile); + t = phone_th = th_create(PHONE_W, PHONE_H); phone_boot(t->screen); th_settle(t, 2000); diff --git a/tests/test-core.c b/tests/test-core.c index 34f04ba5..e618bc0f 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -30,7 +30,7 @@ static twin_pixmap_t *solid(twin_format_t fmt, static void test_damage_merge(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); twin_screen_t *s = t->screen; twin_screen_damage(s, 0, 0, 10, 10); @@ -96,7 +96,7 @@ static void build_scene(th_t *t, twin_pixmap_t **out, int *n) static void test_occlusion_differential(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); twin_pixmap_t *p[8]; int n; build_scene(t, p, &n); @@ -158,7 +158,7 @@ static uint32_t full_frame(th_t *t, unsigned debug) */ static void test_row_fallback(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); twin_pixmap_t *p[8]; int n; build_scene(t, p, &n); @@ -201,8 +201,10 @@ static void test_row_fallback(void) static void test_shadow(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -232,7 +234,7 @@ static void test_shadow(void) static void test_rgb16_and_opacity(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -498,7 +500,7 @@ static bool finish_and_stop(twin_fixed_t v, void *closure) static void test_tween(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -562,7 +564,7 @@ static twin_time_t once(twin_time_t now, void *closure) static void test_closure_churn(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); /* Far more distinct closures than the tracker holds at once */ for (int i = 0; i < 4 * TWIN_MAX_CLOSURES; i++) { void *owner = malloc(16); @@ -619,7 +621,7 @@ static void scroll_draw(twin_window_t *w) static void test_scroll(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -676,7 +678,7 @@ static void probe_draw(twin_window_t *w) static void test_window_damage_rects(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -766,7 +768,7 @@ static void test_translated_source(void) static void test_leaks(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); th_frame(t); size_t base = th_heap_net(t); for (int i = 0; i < 20; i++) { @@ -830,7 +832,7 @@ static twin_dispatch_result_t custom_handler(twin_widget_t *widget, /* Destroying a toplevel releases every widget the toolkit created for it */ static void test_widget_teardown(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); th_frame(t); size_t base = th_heap_net(t); for (int round = 0; round < 3; round++) { @@ -888,7 +890,7 @@ static twin_time_t cancel_victim_tween(twin_time_t now, void *closure) static void test_cleared_in_pass(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); int a, b; victim_runs = 0; twin_set_timeout(clear_victim, 0, &a); @@ -964,7 +966,7 @@ static void stale_draw(twin_window_t *w) static void test_destroyed_paint(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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; @@ -989,7 +991,7 @@ static void on_draw(twin_window_t *window) static void test_accessors(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); CHECK( twin_screen_width(t->screen) == W && twin_screen_height(t->screen) == H, "screen size"); diff --git a/tests/test-demos.c b/tests/test-demos.c index 9346f3be..a820e2fa 100644 --- a/tests/test-demos.c +++ b/tests/test-demos.c @@ -5,9 +5,9 @@ */ /* - * Every app in the registry (apps.h) under each window manager it is marked - * for that this build has: the built policy (desktop or mobile) and 'none'. - * Each app is started on a fresh screen, run for a while, poked, and then + * 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 @@ -18,14 +18,15 @@ #include "harness.h" #if defined(CONFIG_WM_MOBILE) -#define BUILT_WM TwinWmMobile +#define SCENE "mobile" #define W 240 #define H 320 #elif defined(CONFIG_WM_DESKTOP) -#define BUILT_WM TwinWmDesktop +#define SCENE "desktop" #define W 640 #define H 480 #else +#define SCENE "none" #define W 640 #define H 480 #endif @@ -50,7 +51,11 @@ static twin_window_t *top_app(twin_screen_t *screen, int *count) static void check_placement(th_t *t, const char *what) { twin_rect_t area = twin_screen_work_area(t->screen); - bool fills = t->screen->wm->mode != TwinWmDesktop; +#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)) @@ -84,12 +89,12 @@ static bool painted(th_t *t, const twin_window_t *w) return false; } -static void run(const apps_entry_t *d, twin_wm_mode_t mode, const char *scene) +static void run(const apps_entry_t *d) { char what[96]; - snprintf(what, sizeof(what), "%s under %s", d->name, scene); + snprintf(what, sizeof(what), "%s under %s", d->name, SCENE); - th_t *t = th_create(W, H, mode); + 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; @@ -115,10 +120,11 @@ static void run(const apps_entry_t *d, twin_wm_mode_t mode, const char *scene) twin_window_t *top = top_app(t->screen, NULL); if (!top) break; - if (mode == TwinWmMobile) - twin_window_back(top); - else - twin_window_close(top); +#if defined(CONFIG_WM_MOBILE) + twin_window_back(top); +#else + twin_window_close(top); +#endif th_settle(t, 2000); twin_window_t *next = top_app(t->screen, NULL); if (next && next->pixmap && _twin_window_shown(next)) @@ -141,9 +147,9 @@ static void run(const apps_entry_t *d, twin_wm_mode_t mode, const char *scene) /* 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, twin_wm_mode_t mode) +static void run_out_of_memory(const apps_entry_t *d) { - th_t *t = th_create(W, H, mode); + th_t *t = th_create(W, H); th_settle(t, 500); size_t base = th_heap_net(t); _twin_alloc_fail_next = 1; @@ -170,24 +176,10 @@ int main(void) /* Shells own the screen and have suites of their own */ if (d->shell) continue; - /* Each policy the app is marked for that this build has */ - const twin_wm_mode_t modes[] = { -#if defined(BUILT_WM) - BUILT_WM, -#endif - TwinWmNone, - }; - for (size_t m = 0; m < sizeof(modes) / sizeof(modes[0]); m++) { - if (!apps_runs_under(d, modes[m])) - continue; - run(d, modes[m], - modes[m] == TwinWmMobile ? "mobile" - : modes[m] == TwinWmDesktop ? "desktop" - : "none"); + run(d); #if defined(CONFIG_MEMORY_STATS) - run_out_of_memory(d, modes[m]); + 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 index d5bd633a..61e69b5c 100644 --- a/tests/test-lifetime.c +++ b/tests/test-lifetime.c @@ -85,7 +85,7 @@ static void schedule_on_destroy(twin_window_t *w) static void test_window_destroy_schedules(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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; @@ -124,7 +124,7 @@ static twin_dispatch_result_t repaint_on_destroy(twin_widget_t *widget, static void test_toplevel_destroy_repaint(void) { - th_t *t = th_create(W, H, TwinWmNone); + th_t *t = th_create(W, H); twin_toplevel_t *top = twin_toplevel_create( t->screen, TWIN_ARGB32, TwinWindowApplication, 0, 0, W, H, "t"); twin_label_t *label = twin_label_create( @@ -151,7 +151,7 @@ static void test_toplevel_destroy_repaint(void) */ static void test_desktop_destroy_unshown(void) { - th_t *t = th_create(W, H, TwinWmDesktop); + 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); @@ -180,17 +180,18 @@ static void test_desktop_destroy_unshown(void) /* * Under the mobile policy a destroyed app's pixels are adopted to animate * the pop, leaving window->pixmap NULL while the destroy callback runs. - * Switching policy from that callback must skip the window, not crash. + * Re-laying out every window from that callback must skip the window, not + * crash. */ -static void switch_wm_on_destroy(twin_window_t *w) +static void relayout_on_destroy(twin_window_t *w) { CHECK(!w->pixmap, "pixels adopted for the pop animation"); - CHECK(twin_screen_set_wm(w->screen, TwinWmNone), "switch to 'none'"); + twin_screen_set_inset(w->screen, 30); } -static void test_wm_switch_from_destroy(void) +static void test_relayout_from_destroy(void) { - th_t *t = th_create(W, H, TwinWmMobile); + 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); @@ -200,12 +201,12 @@ static void test_wm_switch_from_destroy(void) th_settle(t, 1000); CHECK(b->pixmap->screen && b->pixmap->x == 0, "second app on top"); - b->destroy = switch_wm_on_destroy; + b->destroy = relayout_on_destroy; twin_window_destroy(b); th_settle(t, 1000); - CHECK_EQ(twin_screen_get_wm(t->screen), TwinWmNone, "policy switched"); - CHECK(a->pixmap && a->pixmap->screen, "remaining app shown"); - CHECK_EQ(th_damage_check(t), 0, "screen consistent after the switch"); + 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); @@ -236,7 +237,7 @@ static bool self_finish(twin_fixed_t value, void *closure) static void test_tween_finish_from_proc(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -332,7 +333,7 @@ int main(void) test_desktop_destroy_unshown(); #endif #if defined(CONFIG_WM_MOBILE) - test_wm_switch_from_destroy(); + test_relayout_from_destroy(); #endif test_tween_finish_from_proc(); test_cache_collision(); diff --git a/tests/test-wm.c b/tests/test-wm.c index 39ec7de9..5d6d80b2 100644 --- a/tests/test-wm.c +++ b/tests/test-wm.c @@ -5,9 +5,9 @@ */ /* - * Window manager tests: placement and input under each policy, mobile - * transitions and eviction, and switching policies at run time. Every step - * checks that incremental scan-out matches a from-scratch recomposite. + * 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 @@ -18,12 +18,6 @@ #define W 240 #define H 320 -/* The interactive policy this build has, if any; 'none' is always there */ -#if defined(CONFIG_WM_MOBILE) -#define BUILT_WM TwinWmMobile -#elif defined(CONFIG_WM_DESKTOP) -#define BUILT_WM TwinWmDesktop -#endif #define TOP_BAR 20 #define BOTTOM_BAR 32 @@ -109,7 +103,8 @@ static bool shown(const twin_window_t *w) static void test_none(void) { - th_t *t = th_create(W, H, TwinWmNone); +#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); @@ -139,12 +134,13 @@ static void test_none(void) 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, TwinWmDesktop); + 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); @@ -222,7 +218,7 @@ static bool is_restored(const twin_window_t *w) static void test_desktop_iconify(void) { #if defined(CONFIG_WM_DESKTOP) - th_t *t = th_create(W, H, TwinWmDesktop); + 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 */ @@ -268,7 +264,7 @@ static void test_desktop_iconify(void) */ static void test_restore_handoff(void) { - th_t *t = th_create(W, H, TwinWmNone); + 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); @@ -313,7 +309,7 @@ typedef struct { static void phone_init(phone_t *ph) { memset(ph, 0, sizeof(*ph)); - ph->t = th_create(W, H, TwinWmMobile); + 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); @@ -588,169 +584,6 @@ static void test_mobile_memory(void) } #endif /* CONFIG_WM_MOBILE */ -static void test_switching(void) -{ - th_t *t = th_create(W, H, TwinWmNone); - app_t bars[2] = {{.color = 0xff111111}, {.color = 0xff222222}}; - twin_window_t *bt = mk(t, &bars[0], TwinWindowPlain, 0, 0, W, TOP_BAR); - twin_window_t *bb = mk(t, &bars[1], TwinWindowPlain, 0, 0, W, BOTTOM_BAR); - twin_screen_set_bars(t->screen, bt, bb); - th_frame(t); - size_t base = th_heap_net(t); - - app_t apps[3] = { - {.color = 0xffaa0000}, {.color = 0xff00aa00}, {.color = 0xff0000aa}}; - twin_window_t *w[3]; - for (int i = 0; i < 3; i++) - w[i] = mk(t, &apps[i], TwinWindowApplication, 10, 30, 160, 200); - th_settle(t, 1000); - CHECK(!bt->pixmap->screen, "bars hidden without the mobile WM"); - CONSISTENT(t, "switch: none"); - - const twin_wm_mode_t cycle[] = {TwinWmMobile, TwinWmDesktop, TwinWmNone, - TwinWmDesktop, TwinWmMobile, TwinWmNone}; - for (size_t i = 0; i < sizeof(cycle) / sizeof(cycle[0]); i++) { - twin_wm_mode_t m = cycle[i]; - bool ok = twin_screen_set_wm(t->screen, m); -#if !defined(CONFIG_WM_MOBILE) - if (m == TwinWmMobile) { - CHECK(!ok, "compiled-out mobile WM refused"); - continue; - } -#endif -#if !defined(CONFIG_WM_DESKTOP) - if (m == TwinWmDesktop) { - CHECK(!ok, "compiled-out desktop WM refused"); - continue; - } -#endif - CHECK(ok, "switch to mode %d", m); - CHECK(twin_screen_get_wm(t->screen) == m, "mode %d active", m); - th_settle(t, 1000); - switch (m) { - case TwinWmMobile: - CHECK(shown(bt) && shown(bb), "mobile shows the bars"); - CHECK(shown(w[2]) && !w[0]->pixmap->screen, - "mobile shows only the top app"); - CHECK(w[2]->pixmap->y == TOP_BAR, "top app in the work area"); - break; - case TwinWmDesktop: - CHECK(!bt->pixmap->screen, "desktop hides the bars"); - CHECK(shown(w[0]) && shown(w[1]) && shown(w[2]), - "desktop shows every app"); - CHECK(w[2]->client.top > 0, "desktop decorations"); - CHECK(w[1]->pixmap->x != w[0]->pixmap->x, - "identical requests cascade"); - CHECK(w[2]->active, "topmost app active"); - break; - case TwinWmNone: - CHECK(w[2]->client.top == 0 && w[2]->pixmap->width == W, - "none: undecorated full screen"); - CHECK(t->screen->top == w[2]->pixmap, "stacking order kept"); - break; - } - CONSISTENT(t, "switch: after mode change"); - CHECK(th_pixel(t, W / 2, H / 2) == 0xff0000aa || m == TwinWmDesktop, - "top app visible after switching to %d", m); - } - -#if defined(CONFIG_WM_MOBILE) - /* Switching in the middle of a transition */ - twin_screen_set_wm(t->screen, TwinWmMobile); - th_settle(t, 1000); - app_t extra = {.color = 0xffaaaa00}; - twin_window_t *we = mk(t, &extra, TwinWindowApplication, 0, 0, W, H); - th_advance(t, 48); - twin_screen_set_wm(t->screen, TwinWmNone); - th_settle(t, 1000); - CONSISTENT(t, "switch: mid transition"); - twin_screen_set_wm(t->screen, TwinWmMobile); - twin_window_destroy(we); - th_advance(t, 48); - twin_screen_set_wm(t->screen, TwinWmNone); - th_settle(t, 1000); - CONSISTENT(t, "switch: mid pop"); -#endif - - for (int i = 0; i < 3; i++) - twin_window_destroy(w[i]); - th_settle(t, 1000); - CHECK(th_heap_net(t) <= base, "switching leaks nothing (%zu > %zu)", - th_heap_net(t), base); - twin_window_destroy(bt); - twin_window_destroy(bb); - th_destroy(t); -} - -#if defined(BUILT_WM) -static twin_wm_mode_t switch_to; -static bool dispatch_result; - -static bool switching_event(twin_window_t *w, twin_event_t *ev) -{ - if (ev->kind == TwinEventButtonDown) { - /* Switch while our own event is being delivered */ - dispatch_result = twin_screen_set_wm(w->screen, switch_to); - return true; - } - return app_event(w, ev); -} -#endif - -static void test_switch_from_handler(void) -{ -#if defined(BUILT_WM) - th_t *t = th_create(W, H, BUILT_WM); - app_t a = {.color = 0xffaa3333}; - twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); - w->event = switching_event; - th_settle(t, 1000); - - switch_to = TwinWmNone; - th_press(t, 100, 100); - CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, - "pending switch reported at once"); - th_release(t, 100, 100); - th_settle(t, 1000); - CHECK(t->screen->wm->mode == TwinWmNone, "switch applied after the event"); - CHECK(w->client.top == 0 && w->pixmap->width == W, - "window full screen after the switch"); - CONSISTENT(t, "switch from handler"); - - switch_to = BUILT_WM; - th_tap(t, 100, 150); - th_settle(t, 1000); - CHECK(t->screen->wm->mode == BUILT_WM, "switched back from a handler"); - twin_window_destroy(w); - -#if defined(CONFIG_WM_DESKTOP) - /* Phone-sized screens built for the work area cascade and stay on screen, - * also after a round trip through 'none' */ - app_t apps[3] = { - {.color = 0xff111111}, {.color = 0xff222222}, {.color = 0xff333333}}; - twin_window_t *ws[3]; - for (int i = 0; i < 3; i++) - ws[i] = mk(t, &apps[i], TwinWindowApplication, 0, 20, W, 258); - twin_screen_set_wm(t->screen, TwinWmNone); - twin_screen_set_wm(t->screen, TwinWmDesktop); - th_settle(t, 1000); - for (int i = 0; i < 3; i++) { - twin_pixmap_t *p = ws[i]->pixmap; - CHECK(p->x >= 0 && p->y >= 0 && p->x + p->width <= W && - p->y + p->height <= H, - "window %d on screen (%d,%d %dx%d)", i, p->x, p->y, p->width, - p->height); - } - CHECK(ws[0]->pixmap->y != ws[1]->pixmap->y, "second window cascaded"); - CONSISTENT(t, "cascade on a small screen"); - for (int i = 0; i < 3; i++) - twin_window_destroy(ws[i]); -#endif - th_destroy(t); -#endif -} - - /* Without a window manager, hiding the focused window hands the keyboard * to the topmost window still shown */ static int self_destroyed; @@ -771,7 +604,9 @@ static bool destroy_on(twin_window_t *w, twin_event_t *ev) * freed only once it is idle; nothing touches them after */ static void test_destroy_in_callbacks(void) { - th_t *t = th_create(W, H, TwinWmNone); +#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); @@ -801,6 +636,7 @@ static void test_destroy_in_callbacks(void) 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 */ @@ -826,7 +662,8 @@ static void test_destroy_in_callbacks(void) static void test_none_focus(void) { - th_t *t = th_create(W, H, TwinWmNone); +#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); @@ -839,6 +676,7 @@ static void test_none_focus(void) twin_window_destroy(wb); twin_window_destroy(wa); th_destroy(t); +#endif } #if defined(CONFIG_WM_DESKTOP) @@ -878,7 +716,7 @@ static bool leave_destroys(twin_window_t *w, twin_event_t *ev) static void test_desktop_focus(void) { #if defined(CONFIG_WM_DESKTOP) - th_t *t = th_create(W, H, TwinWmDesktop); + 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}; @@ -931,7 +769,7 @@ static void test_desktop_focus(void) static void test_desktop_moves(void) { #if defined(CONFIG_WM_DESKTOP) - th_t *t = th_create(W, H, TwinWmDesktop); + 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); @@ -1069,7 +907,7 @@ static void test_screen_resize(void) #endif #if defined(CONFIG_WM_DESKTOP) { - th_t *t = th_create(W, H, TwinWmDesktop); + 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); @@ -1223,79 +1061,6 @@ static void test_mobile_bars(void) } #endif -static void test_switch_pending(void) -{ -#if defined(BUILT_WM) - th_t *t = th_create(W, H, BUILT_WM); - app_t a = {.color = 0xffaa3333}; - twin_window_t *w = mk(t, &a, TwinWindowApplication, 0, 20, W, 258); - w->event = switching_event; - th_settle(t, 1000); - - /* A direct request replaces one queued by a handler */ - switch_to = TwinWmNone; - twin_event_t ev = {.kind = TwinEventButtonDown}; - ev.u.pointer.screen_x = 100; - ev.u.pointer.screen_y = 100; - twin_screen_dispatch(t->screen, &ev); - CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "queued"); - CHECK(twin_screen_set_wm(t->screen, BUILT_WM), "direct switch"); - th_settle(t, 1000); - CHECK(t->screen->wm->mode == BUILT_WM, "stale queued switch dropped"); - th_release(t, 100, 100); - - /* The policy that is not built is refused, from a handler too */ - switch_to = BUILT_WM == TwinWmMobile ? TwinWmDesktop : TwinWmMobile; - dispatch_result = true; - th_tap(t, 100, 100); - CHECK(!dispatch_result, "compiled-out policy refused from a handler"); - th_settle(t, 1000); - CHECK(t->screen->wm->mode == BUILT_WM, "current policy kept"); - -#if defined(CONFIG_WM_MOBILE) - /* Switching while a swipe is under the finger */ - w->event = app_event; - app_t b = {.color = 0xff33aa33}; - twin_window_t *wb = mk(t, &b, TwinWindowApplication, 0, 20, W, 258); - th_settle(t, 1000); - th_press(t, 3, 150); - th_move(t, 60, 150); - twin_screen_set_wm(t->screen, TwinWmNone); - th_move(t, 90, 150); - th_release(t, 90, 150); - th_settle(t, 1000); - CONSISTENT(t, "switch mid swipe"); - twin_window_destroy(wb); - twin_screen_set_wm(t->screen, BUILT_WM); - th_settle(t, 1000); -#endif - -#if defined(CONFIG_MEMORY_STATS) - /* Out of memory: a handler's switch is refused, not forced */ - w->event = switching_event; - switch_to = TwinWmNone; - _twin_alloc_fail_next = 1; - dispatch_result = true; - th_tap(t, 100, 100); - _twin_alloc_fail_next = 0; - CHECK(!dispatch_result, "queuing failure reported"); - th_settle(t, 1000); - CHECK(t->screen->wm->mode == BUILT_WM, "current policy kept"); - - /* A policy that cannot start falls back to none */ - twin_screen_set_wm(t->screen, TwinWmNone); - _twin_alloc_fail_next = 1; - CHECK(!twin_screen_set_wm(t->screen, BUILT_WM), "failed start"); - _twin_alloc_fail_next = 0; - CHECK(twin_screen_get_wm(t->screen) == TwinWmNone, "fell back to none"); - th_settle(t, 1000); - CONSISTENT(t, "after failed switches"); -#endif - twin_window_destroy(w); - th_destroy(t); -#endif -} - int main(void) { test_none(); @@ -1317,9 +1082,6 @@ int main(void) test_desktop_focus(); test_desktop_moves(); test_screen_resize(); - test_switching(); - test_switch_from_handler(); - test_switch_pending(); twin_cache_flush(); return th_report("wm"); } From df5a6617c1eb53c8d02129d3ca3d6a9a8989696e Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 22:07:32 +0800 Subject: [PATCH 17/48] Share demo test helpers and trim repaint work A toplevel is shown by drawing through its window, so its widgets are painted once and the window manager's first draw finds no damage left; before, every new toplevel was painted twice on a desktop. The animation's black margin is its widget background instead of a second fill per frame, the multi demo paints text straight into its windows instead of through a full-size mask per draw, and its blur window reuses start_window(). The calculator installs the callback that frees it last, so every failure frees it the same way. Bars are hidden once when registered rather than on every relayout. The phone shell has one home-window check and lists the registry apps at boot. Settings drops the leftovers of its window-manager page: a dead return, a one-member enum and a copy of ui_header(). The tests share th_top_app(), th_painted() and th_crc_rect() through the harness, the edge swipe uses ph_drag(), and home.c exports the icon rectangle so the phone test no longer copies the home grid geometry. --- apps/animation.c | 7 ++-- apps/calc.c | 12 +++---- apps/cellphone/home.c | 11 ++++++ apps/cellphone/settings.c | 26 ++++---------- apps/cellphone/shell.c | 36 +++++++++---------- apps/multi.c | 54 ++++++++++++---------------- src/toplevel.c | 9 +++-- src/wm.c | 15 ++++---- tests/harness.c | 31 +++++++++++++++- tests/harness.h | 10 ++++++ tests/phone-demos.c | 76 +++++++++++---------------------------- tests/test-demos.c | 43 +++++----------------- 12 files changed, 149 insertions(+), 181 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index abe3829c..cd9eceff 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -33,7 +33,8 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) 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 */ + * 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); @@ -41,8 +42,6 @@ static void _apps_animation_paint(twin_custom_widget_t *custom) 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; - if (w > fw || h > fh) - twin_fill(dst, 0xff000000, TWIN_SOURCE, 0, 0, w, h); twin_operand_t srcop = { .source_kind = TWIN_PIXMAP, @@ -97,7 +96,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, 1, 1, _apps_animation_dispatch, sizeof(apps_animation_data_t)); if (!custom) return NULL; diff --git a/apps/calc.c b/apps/calc.c index 61b08680..687ef30f 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -225,17 +225,12 @@ bool apps_calc_start(twin_screen_t *screen, return false; } twin_box_t *top = twin_toplevel_box(calc->toplevel); - /* The display frees the calculator when the window goes away */ - bool owned = false; calc->display = twin_label_create(top, "0", APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, APPS_CALC_VALUE_STYLE); if (!calc->display) goto fail; twin_widget_t *display = twin_label_widget(calc->display); - owned = twin_widget_set_callback(display, _apps_calc_display_event, calc); - if (!owned) - goto fail; twin_widget_set(display, APPS_CALC_VALUE_BG); twin_label_set_align(calc->display, TwinAlignRight); twin_widget_set_shape(display, TwinShapeLozenge); @@ -264,6 +259,10 @@ bool apps_calc_start(twin_screen_t *screen, } } + /* 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; @@ -273,7 +272,6 @@ bool apps_calc_start(twin_screen_t *screen, fail: twin_toplevel_destroy(calc->toplevel); - if (!owned) - free(calc); + free(calc); return false; } diff --git a/apps/cellphone/home.c b/apps/cellphone/home.c index aaf3a3d3..5cac8cd9 100644 --- a/apps/cellphone/home.c +++ b/apps/cellphone/home.c @@ -193,3 +193,14 @@ ui_view_t *home_create(void) h->pressed = -1; return &h->view; } + +/* Test hook: @app's icon in home-screen content coordinates; false unless + * home is on top with that icon'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_plate(v, app); + return out->right > out->left; +} diff --git a/apps/cellphone/settings.c b/apps/cellphone/settings.c index 9af9bbfc..f2f7e020 100644 --- a/apps/cellphone/settings.c +++ b/apps/cellphone/settings.c @@ -62,7 +62,7 @@ static const char *const page_titles[] = { typedef enum { IT_SECTION, IT_LINK, /* id: page */ - IT_RADIO, /* id: group, arg: value */ + IT_RADIO, /* arg: theme */ IT_SWITCH, /* id: pref */ IT_SLIDER, /* id: pref */ IT_INFO, /* id: value */ @@ -71,8 +71,6 @@ typedef enum { IT_CLOCK, /* large time and date */ } item_kind_t; -enum { GROUP_THEME }; - /* Info values; those from VAL_HEAP on are refreshed once per second */ enum { VAL_NAME, @@ -173,7 +171,6 @@ static void format_value(int id, char *buf, size_t len) case VAL_SCREEN: snprintf(buf, len, "%dx%d", PHONE_W, PHONE_H); return; - return; case VAL_TZ: snprintf(buf, len, "GMT+8 Taipei"); return; @@ -266,8 +263,7 @@ static void build(page_t *s) case PAGE_DISPLAY: section(s, "Theme"); for (int i = 0; i < phone_theme_count(); i++) - add(s, IT_RADIO, GROUP_THEME, i, NULL, 0, phone_theme_at(i)->name, - NULL); + add(s, IT_RADIO, 0, i, NULL, 0, phone_theme_at(i)->name, NULL); section(s, "Screen"); add(s, IT_SLIDER, PREF_BRIGHTNESS, 0, NULL, 0, "Brightness", NULL); break; @@ -420,17 +416,6 @@ static ui_rect_t value_rect(ui_rect_t row) /* * Painting */ -static void paint_header(page_t *s, twin_pixmap_t *px) -{ - const phone_theme_t *th = phone_theme(); - twin_coord_t w = ui_view_width(&s->view); - ui_rect_t r = ui_rect(0, 0, w, HEADER_H); - twin_coord_t x = 6; - ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 96)); - ui_text_in(px, FONT_HEADING, ui_rect(x, 0, w - x - 8, HEADER_H), UI_LEFT, - th->text, s->view.title); - ui_separator(px, r); -} static void paint_plate(twin_pixmap_t *px, twin_coord_t cy, @@ -455,7 +440,7 @@ static void paint_swatch(twin_pixmap_t *px, twin_coord_t cy, int theme) static bool radio_on(const item_t *it) { - return it->id == GROUP_THEME && phone_theme_index() == it->arg; + return phone_theme_index() == it->arg; } static void link_detail(const item_t *it, char *buf, size_t len) @@ -489,7 +474,7 @@ static void paint_row(page_t *s, twin_pixmap_t *px, int i, ui_rect_t r) if (it->sym) { paint_plate(px, cy, it->sym, it->accent); x = 10 + PLATE + 10; - } else if (it->kind == IT_RADIO && it->id == GROUP_THEME) { + } else if (it->kind == IT_RADIO) { paint_swatch(px, cy, it->arg); x = 10 + PLATE + 10; } @@ -681,7 +666,8 @@ static void settings_paint(ui_view_t *v, twin_pixmap_t *px) } /* Last, so rows scrolled under the header never show through */ if (clip.top < HEADER_H) - paint_header(s, px); + ui_header(px, ui_rect(0, 0, ui_view_width(&s->view), HEADER_H), + s->view.title, NULL); } /* diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index e83616d7..3b9cc92e 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -292,14 +292,12 @@ static const phone_app_t apps[] = { #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 */ + * a mobile build compiles in only apps that run under it. Listed at boot. */ static phone_app_t demos[16]; -static int n_demos = -1; +static int n_demos; static void _list_demos(void) { - if (n_demos >= 0) - return; static const twin_argb32_t accents[] = { UI_RGB(0x0f766e), UI_RGB(0x78716c), UI_RGB(0xa855f7), UI_RGB(0xd946ef), UI_RGB(0x84cc16), UI_RGB(0x0ea5e9), @@ -323,7 +321,6 @@ static void _list_demos(void) int phone_app_count(void) { - _list_demos(); return N_APPS + n_demos; } @@ -331,7 +328,6 @@ const phone_app_t *phone_app_at(int idx) { if (idx >= 0 && idx < N_APPS) return &apps[idx]; - _list_demos(); return idx >= N_APPS && idx < N_APPS + n_demos ? &demos[idx - N_APPS] : NULL; } @@ -512,14 +508,24 @@ static int nav_hit(twin_coord_t x, twin_coord_t y) return -1; } -static bool is_app_window(twin_window_t *w); +/* 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) && (!g_home || w != g_home->window)) + if (w != keep && is_app_window(w) && !is_home(w)) twin_window_destroy(w); w = next; } @@ -568,13 +574,6 @@ static bool nav_event(twin_window_t *w, twin_event_t *ev) /* * Navigation */ -/* 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 twin_window_t *_top_app(void) { twin_window_t *top = NULL; @@ -600,7 +599,7 @@ int phone_depth(void) return 0; for (twin_window_t *w = twin_screen_first_window(g_screen); w; w = twin_window_next(w)) - if (is_app_window(w) && (!g_home || w != g_home->window)) + if (is_app_window(w) && !is_home(w)) n++; return n; } @@ -629,14 +628,14 @@ void phone_open(int app) void phone_back(void) { twin_window_t *top = _top_app(); - if (top && (!g_home || top != g_home->window)) + if (top && !is_home(top)) twin_window_back(top); } void phone_home(void) { twin_window_t *top = _top_app(); - if (!top || (g_home && top == g_home->window)) + if (!top || is_home(top)) return; /* Hidden screens go first (no animation), then the visible one pops */ _drop_apps(top); @@ -713,6 +712,7 @@ 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); diff --git a/apps/multi.c b/apps/multi.c index 293a418a..b8cf5656 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -200,39 +200,35 @@ static twin_fixed_t fit_400(int w, int h) : twin_fixed_div(twin_int_to_fixed(side), D(400)); } +/* 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) + twin_draw_func_t draw, + void *data, + twin_destroy_func_t destroy) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); if (!window) return false; twin_window_set_name(window, name); - twin_window_set_callbacks(window, draw, NULL, NULL); - twin_rect_t c = twin_window_client(window); - twin_window_damage(window, c.left, c.top, c.right, c.bottom); + twin_window_set_client_data(window, data); + twin_window_set_callbacks(window, draw, NULL, destroy); twin_window_draw(window); twin_window_show(window); return true; } -/* Fill @path's coverage into the window in black over white */ +/* Paint @path in black over white */ static void paint_text(twin_pixmap_t *pixmap, twin_path_t *path, int w, int h) { twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); - twin_pixmap_t *alpha = twin_pixmap_create(TWIN_A8, w, h); - if (!alpha) - return; - twin_paint_path(alpha, 0xff000000, path); - twin_operand_t source = {.source_kind = TWIN_SOLID, .u.argb = 0xff000000}; - twin_operand_t mask = {.source_kind = TWIN_PIXMAP, .u.pixmap = alpha}; - twin_composite(pixmap, 0, 0, &source, 0, 0, &mask, 0, 0, TWIN_OVER, w, h); - twin_pixmap_destroy(alpha); + twin_paint_path(pixmap, 0xff000000, path); } static void circletext_draw(twin_window_t *window) @@ -483,20 +479,11 @@ static bool apps_blur(twin_screen_t *screen, int x, int y, int w, int h) #endif if (!raw_background) return false; - twin_window_t *window = twin_window_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); - if (!window) { - twin_pixmap_destroy(raw_background); - return false; - } - twin_window_set_name(window, "Blur"); - twin_window_set_client_data(window, raw_background); - twin_window_set_callbacks(window, blur_draw, NULL, blur_destroy); - 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); - return true; + if (start_window(screen, x, y, w, h, "Blur", blur_draw, raw_background, + blur_destroy)) + return true; + twin_pixmap_destroy(raw_background); + return false; } bool apps_multi_start(twin_screen_t *screen, @@ -508,12 +495,15 @@ bool apps_multi_start(twin_screen_t *screen, { (void) name; /* Started if any of its windows opened */ - bool any = start_window(screen, x, y, w, h, "circletext", circletext_draw); + bool any = start_window(screen, x, y, w, h, "circletext", circletext_draw, + NULL, NULL); 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); - any |= start_window(screen, x += 20, y += 20, w, h, "ASCII", ascii_draw); - any |= start_window(screen, x += 20, y += 20, w, h, "Flower", flower_draw); + any |= start_window(screen, x += 20, y += 20, 200, 200, "Smiley", + smiley_draw, NULL, NULL); + any |= start_window(screen, x += 20, y += 20, w, h, "ASCII", ascii_draw, + NULL, NULL); + any |= start_window(screen, x += 20, y += 20, w, h, "Flower", flower_draw, + NULL, NULL); any |= apps_blur(screen, x += 20, y += 20, w / 2, h / 2); return any; } diff --git a/src/toplevel.c b/src/toplevel.c index 3da26c8d..dd404ce3 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -179,9 +179,14 @@ void _twin_toplevel_queue_layout(twin_widget_t *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) diff --git a/src/wm.c b/src/wm.c index d05c818c..52636e3d 100644 --- a/src/wm.c +++ b/src/wm.c @@ -89,13 +89,6 @@ void _twin_screen_relayout(twin_screen_t *screen) for (twin_window_t *w = screen->windows; w; w = w->wm_next) if (w->pixmap) _twin_window_relayout(w); - /* Bars belong to policies that carve a work area out for them */ - if (!screen->wm->work_area) { - if (screen->bar_top && screen->bar_top->pixmap) - twin_pixmap_hide(screen->bar_top->pixmap); - if (screen->bar_bottom && screen->bar_bottom->pixmap) - twin_pixmap_hide(screen->bar_bottom->pixmap); - } if (screen->wm->refresh) screen->wm->refresh(screen); twin_screen_enable_update(screen); @@ -125,6 +118,14 @@ void twin_screen_set_bars(twin_screen_t *screen, if (old[i] && old[i] != top && old[i] != bottom) twin_window_hide(old[i]); } + /* Bars belong to policies that carve a work area out for them; the + * others never map a registered bar, so hiding it once is enough */ + if (!screen->wm->work_area) { + if (top && top->pixmap) + twin_pixmap_hide(top->pixmap); + if (bottom && bottom->pixmap) + twin_pixmap_hide(bottom->pixmap); + } _twin_screen_relayout(screen); } diff --git a/tests/harness.c b/tests/harness.c index 15100955..3fa667ef 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -194,7 +194,7 @@ static uint32_t th_crc_bytes(const void *data, size_t len) return ~crc_update(~0u, data, len); } -static uint32_t th_crc_rect(const th_t *t, twin_rect_t r) +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++) @@ -213,6 +213,35 @@ 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; diff --git a/tests/harness.h b/tests/harness.h index a3a9a2e8..62883819 100644 --- a/tests/harness.h +++ b/tests/harness.h @@ -65,6 +65,16 @@ 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. diff --git a/tests/phone-demos.c b/tests/phone-demos.c index bc018183..440c7744 100644 --- a/tests/phone-demos.c +++ b/tests/phone-demos.c @@ -14,59 +14,22 @@ #include "phone-test.h" -/* Home screen grid (home.c): 3x3 icons per page below a 6 px margin */ -#define COLS 3 -#define PER_PAGE 9 -#define CELL_W (PHONE_W / COLS) -#define CELL_H 74 -#define GRID_TOP 6 +bool app_home_icon_rect(int app, ui_rect_t *out); static uint32_t nav_crc(void) { - uint32_t crc = 0; - for (twin_coord_t y = PHONE_H - PHONE_NAV_H; y < PHONE_H; y++) - for (twin_coord_t x = 0; x < PHONE_W; x++) - crc = crc * 31 + th_pixel(phone_th, x, y); - return crc; + return th_crc_rect( + phone_th, (twin_rect_t) {0, PHONE_W, PHONE_H - PHONE_NAV_H, PHONE_H}); } -static void tap_icon(int app) +/* Tap @app's icon; false when it is not on the page shown */ +static bool tap_icon(int app) { - int k = app % PER_PAGE; - ph_tap((k % COLS) * CELL_W + CELL_W / 2, - GRID_TOP + (k / COLS) * CELL_H + CELL_H / 2 - 8); -} - -/* Not one flat colour: the window was painted at its current size */ -static bool painted(const twin_pixmap_t *p) -{ - twin_argb32_t first = th_pixel(phone_th, p->x, p->y); - for (twin_coord_t y = p->y; y < p->y + p->height; y++) - for (twin_coord_t x = p->x; x < p->x + p->width; x++) - if (th_pixel(phone_th, x, y) != first) - return true; - return false; -} - -static twin_window_t *top_window(void) -{ - twin_window_t *top = NULL; - for (twin_window_t *w = phone_screen()->windows; w; w = w->wm_next) - if (w->mapped && w->style == TwinWindowApplication) - top = w; - return top; -} - -/* Swipe from the left edge across the screen: the mobile back gesture */ -static void edge_swipe(void) -{ - th_press(phone_th, 2, CY(120)); - for (twin_coord_t x = 30; x <= 210; x += 30) { - th_move(phone_th, x, CY(120)); - th_advance(phone_th, 16); - } - th_release(phone_th, 210, CY(120)); - th_settle(phone_th, 2000); + 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) @@ -83,8 +46,12 @@ void test_demos(void) uint32_t home_nav = nav_crc(); /* The demos follow the phone apps: page to the first one */ - for (int p = 0; p < first / PER_PAGE; p++) + ui_rect_t r; + int pages = 0; + while (!app_home_icon_rect(first, &r) && pages < 8) { ph_drag(200, 120, 20, 120); + pages++; + } /* The page lists whichever apps are built: the golden frame holds the * default set */ #if defined(CONFIG_DEMO_CLOCK) && defined(CONFIG_DEMO_CALCULATOR) && \ @@ -101,12 +68,11 @@ void test_demos(void) 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 (i / PER_PAGE != first / PER_PAGE) + if (!tap_icon(i)) break; /* the icons below are tested on this page only */ - tap_icon(i); 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 = top_window(); + 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, @@ -123,14 +89,14 @@ void test_demos(void) p = w ? w->pixmap : NULL; CHECK(p && p->height == PHONE_CONTENT_H - inset, "%s: height with inset %d", name, inset); - CHECK(p && painted(p), "%s: painted 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(); - edge_swipe(); + 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++) { @@ -154,6 +120,6 @@ void test_demos(void) th_settle(phone_th, 2000); ph_home(); - for (int p = 0; p < first / PER_PAGE; p++) + while (pages-- > 0) ph_drag(20, 120, 200, 120); } diff --git a/tests/test-demos.c b/tests/test-demos.c index a820e2fa..90ea0c66 100644 --- a/tests/test-demos.c +++ b/tests/test-demos.c @@ -31,21 +31,6 @@ #define H 480 #endif -/* Topmost shown application window, and how many there are */ -static twin_window_t *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; -} - /* 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) @@ -76,19 +61,6 @@ static void check_placement(th_t *t, const char *what) } } -/* A window the policy just revealed was painted again: a released - * (evicted) window that nobody repainted comes back as one flat colour */ -static bool painted(th_t *t, const twin_window_t *w) -{ - twin_pixmap_t *p = w->pixmap; - twin_argb32_t first = th_pixel(t, p->x, p->y); - for (twin_coord_t y = p->y; y < p->y + p->height && y < t->height; y++) - for (twin_coord_t x = p->x; x < p->x + p->width && x < t->width; x++) - if (th_pixel(t, x, y) != first) - return true; - return false; -} - static void run(const apps_entry_t *d) { char what[96]; @@ -104,7 +76,7 @@ static void run(const apps_entry_t *d) CHECK(d->start(t->screen, d->x, d->y, d->w, d->h), "%s: started", what); th_settle(t, 2000); int n; - CHECK(top_app(t->screen, &n) && n > 0, "%s: opened a window", what); + 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); @@ -117,7 +89,7 @@ static void run(const apps_entry_t *d) /* Close what the demo opened, one window at a time */ for (int guard = 0; guard < 16; guard++) { - twin_window_t *top = top_app(t->screen, NULL); + twin_window_t *top = th_top_app(t->screen, NULL); if (!top) break; #if defined(CONFIG_WM_MOBILE) @@ -126,11 +98,12 @@ static void run(const apps_entry_t *d) twin_window_close(top); #endif th_settle(t, 2000); - twin_window_t *next = top_app(t->screen, NULL); + twin_window_t *next = th_top_app(t->screen, NULL); if (next && next->pixmap && _twin_window_shown(next)) - CHECK(painted(t, next), "%s: revealed window painted", what); + CHECK(th_painted(t, next->pixmap), + "%s: revealed window painted", what); } - CHECK(!top_app(t->screen, &n), "%s: every window closed (%d left)", + CHECK(!th_top_app(t->screen, &n), "%s: every window closed (%d left)", what, n); CHECK_EQ(th_damage_check(t), 0, what); } @@ -156,11 +129,11 @@ static void run_out_of_memory(const apps_entry_t *d) bool ok = d->start(t->screen, d->x, d->y, d->w, d->h); _twin_alloc_fail_next = 0; int n; - top_app(t->screen, &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 = top_app(t->screen, NULL));) + 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, From 457585198accf5301f4d9fa982a0618a656dff75 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 22:16:40 +0800 Subject: [PATCH 18/48] Hide bars again on every relayout A bar's pixmap can still be shown directly through the pixmap API, so hiding registered bars only when they are registered let one stay visible. Re-hide them on every relayout, as before the cleanup. --- src/wm.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wm.c b/src/wm.c index 52636e3d..b7b0e7f3 100644 --- a/src/wm.c +++ b/src/wm.c @@ -89,6 +89,14 @@ void _twin_screen_relayout(twin_screen_t *screen) for (twin_window_t *w = screen->windows; w; w = w->wm_next) if (w->pixmap) _twin_window_relayout(w); + /* Bars belong to policies that carve a work area out for them; a bar + * shown some other way (its pixmap shown directly) goes again here */ + if (!screen->wm->work_area) { + if (screen->bar_top && screen->bar_top->pixmap) + twin_pixmap_hide(screen->bar_top->pixmap); + if (screen->bar_bottom && screen->bar_bottom->pixmap) + twin_pixmap_hide(screen->bar_bottom->pixmap); + } if (screen->wm->refresh) screen->wm->refresh(screen); twin_screen_enable_update(screen); @@ -118,14 +126,6 @@ void twin_screen_set_bars(twin_screen_t *screen, if (old[i] && old[i] != top && old[i] != bottom) twin_window_hide(old[i]); } - /* Bars belong to policies that carve a work area out for them; the - * others never map a registered bar, so hiding it once is enough */ - if (!screen->wm->work_area) { - if (top && top->pixmap) - twin_pixmap_hide(top->pixmap); - if (bottom && bottom->pixmap) - twin_pixmap_hide(bottom->pixmap); - } _twin_screen_relayout(screen); } From ada8aca0ad8a4c1b463810c6d8c3e2ea3919b59a Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 22:45:13 +0800 Subject: [PATCH 19/48] Reject malformed GIFs instead of hanging Counting frames treated gif_get_frame()'s error return as a frame, and unchecked reads left the separator, block sizes and palettes stale or uninitialized at end of file, so a truncated GIF could loop forever. Reads now go through gif_read(), which zero-fills what a short file lacks, and a zero size or separator ends decoding. Frames decoded before a truncation are kept and a file with none is rejected. Sizes that do not fit a pixmap are rejected, size products are computed in size_t, an LZW string running past its frame ends the stream, and the LZW table allocation is checked. test-core loads three malformed files and every prefix of a small image; before the fix the run never finished. --- src/image-gif.c | 87 +++++++++++++++++++++++++++++++++-------------- tests/test-core.c | 57 +++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/src/image-gif.c b/src/image-gif.c index 4c5e7e57..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); diff --git a/tests/test-core.c b/tests/test-core.c index e618bc0f..f83c7821 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -12,6 +12,7 @@ #include #include +#include #include "harness.h" @@ -1068,6 +1069,59 @@ static void test_accessors(void) th_destroy(t); } +#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"; + int fd = mkstemp(path); + if (fd < 0) + return NULL; + bool ok = write(fd, bytes, len) == (ssize_t) len; + close(fd); + twin_pixmap_t *p = ok ? twin_pixmap_from_file(path, TWIN_ARGB32) : NULL; + 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, ';'}; + int returned = 0; + for (size_t cut = 0; cut <= sizeof(good); cut++) { + twin_pixmap_t *p = load_bytes(good, cut); + if (p) + twin_pixmap_destroy(p); + returned++; + } + CHECK_EQ(returned, (int) sizeof(good) + 1, "every truncated GIF returns"); +} +#endif + int main(void) { test_damage_merge(); @@ -1091,6 +1145,9 @@ int main(void) test_accessors(); #if defined(CONFIG_WIDGETS) test_widget_teardown(); +#endif +#if defined(CONFIG_LOADER_GIF) + test_gif_malformed(); #endif test_leaks(); twin_cache_flush(); From 2560e685fec546cbbdf441cf1292c90517ca868a Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 22:45:13 +0800 Subject: [PATCH 20/48] Poll Linux input on the main thread The fbdev and DRM backends read evdev on a second thread that called twin_screen_dispatch() while the main thread ran timeouts and composited, with no locking. Destroying it freed the state the thread was still using and closed descriptor 0, and rescanning devices leaked a descriptor per device each time. Input is now polled on the caller's thread: twin_linux_input_poll() sleeps until input or the next timeout and dispatches what arrived, so fbdev and DRM no longer spin. linux_vt.h passed a pointer to the saved-mode pointer to VT_GETMODE, so the mode put back on exit was zeros (and overran a 4-byte slot on 32-bit targets); it also leaked the VT0 descriptor and could not detect open failures. VNC re-initialised its damage region on every update without freeing it, read an uninitialised event when the button and position were unchanged, dropped a press that came with a move, and polled without sleeping. Backends sleep with _twin_idle_timeout(). twin_run() runs the dispatch loop itself unless the backend owns its loop (WebAssembly), so the five identical start() functions are gone, as is the configure hook nothing called. dispatch.c is folded into api.c. --- Makefile | 1 - backend/drm.c | 40 +---- backend/fbdev.c | 51 +------ backend/headless.c | 45 +----- backend/linux_input.c | 332 ++++++++++++++++++------------------------ backend/linux_input.h | 17 ++- backend/linux_vt.h | 47 +++--- backend/sdl.c | 37 +---- backend/vnc.c | 88 ++++------- backend/wasm.c | 10 -- src/api.c | 78 ++-------- src/dispatch.c | 86 ----------- src/timeout.c | 10 ++ src/twin_private.h | 18 ++- 14 files changed, 274 insertions(+), 586 deletions(-) delete mode 100644 src/dispatch.c diff --git a/Makefile b/Makefile index fbbc1098..abee9061 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,6 @@ libtwin.a_files-y = \ src/screen.c \ src/window.c \ src/wm.c \ - src/dispatch.c \ src/geom.c \ src/pattern.c \ src/spline.c \ diff --git a/backend/drm.c b/backend/drm.c index 5a1add8e..58cac5b6 100644 --- a/backend/drm.c +++ b/backend/drm.c @@ -30,7 +30,7 @@ typedef struct { twin_screen_t *screen; /* Linux input system */ - void *input; + twin_linux_input_t *input; /* Linux virtual terminal (VT) */ int vt_fd; @@ -56,12 +56,6 @@ typedef struct { bool first_run; } 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); @@ -447,8 +441,7 @@ twin_context_t *twin_drm_init(int width maybe_unused, int height maybe_unused) 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); + twin_vt_restore(tx->vt_fd, &tx->old_vtm); close(tx->vt_fd); bail_dri_dev_fd: close(tx->drm_dri_fd); @@ -458,14 +451,6 @@ twin_context_t *twin_drm_init(int width maybe_unused, int height maybe_unused) 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) @@ -475,8 +460,7 @@ static void twin_drm_exit(twin_context_t *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); + twin_vt_restore(tx->vt_fd, &tx->old_vtm); } drmModeFreeCrtc(CRTC(tx)); @@ -494,27 +478,19 @@ static void twin_drm_exit(twin_context_t *ctx) * 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) +/* Sleep in the input poll until input arrives or there is something to + * show; input is dispatched on this thread */ +static bool twin_drm_poll(twin_context_t *ctx) { + twin_linux_input_poll(PRIV(ctx)->input, + (int) _twin_idle_timeout(SCREEN(ctx), -1)); 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, .exit = twin_drm_exit, }; diff --git a/backend/fbdev.c b/backend/fbdev.c index 2a2f5826..8f9cfb38 100644 --- a/backend/fbdev.c +++ b/backend/fbdev.c @@ -25,7 +25,7 @@ typedef struct { twin_screen_t *screen; /* Linux input system */ - void *input; + twin_linux_input_t *input; /* Linux virtual terminal (VT) */ int vt_fd; @@ -85,16 +85,6 @@ 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) -{ - struct fb_var_screeninfo info; - ioctl(tx->fb_fd, FBIOGET_VSCREENINFO, &info); - *width = info.xres; - *height = info.yres; -} - static inline bool twin_fbdev_is_rgb565(twin_fbdev_t *tx) { return tx->fb_var.red.offset == 11 && tx->fb_var.red.length == 5 && @@ -303,8 +293,7 @@ twin_context_t *twin_fbdev_init(int width maybe_unused, int height maybe_unused) 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); + twin_vt_restore(tx->vt_fd, &tx->old_vtm); close(tx->vt_fd); bail_fb_fd: close(tx->fb_fd); @@ -314,14 +303,6 @@ 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) @@ -331,8 +312,7 @@ static void twin_fbdev_exit(twin_context_t *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); + twin_vt_restore(tx->vt_fd, &tx->old_vtm); } munmap(tx->fb_base, tx->fb_len); @@ -347,33 +327,18 @@ static void twin_fbdev_exit(twin_context_t *ctx) * 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) +/* Sleep in the input poll until input arrives or there is something to + * show; input is dispatched on this thread */ +static bool twin_fbdev_poll(twin_context_t *ctx) { + twin_linux_input_poll(PRIV(ctx)->input, + (int) _twin_idle_timeout(SCREEN(ctx), -1)); 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, .exit = twin_fbdev_exit, }; diff --git a/backend/headless.c b/backend/headless.c index 99ef06e9..ebfa0adb 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -254,14 +254,6 @@ static void twin_headless_update_damage(void *closure) } } -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; -} - static bool twin_headless_poll(twin_context_t *ctx) { twin_headless_t *tx = ctx->priv; @@ -281,9 +273,7 @@ static bool twin_headless_poll(twin_context_t *ctx) else { /* Idle: sleep until the next timeout, but stay responsive to the * control tool. */ - twin_time_t wait = _twin_timeout_delay(); - if (wait < 0 || wait > 5) - wait = 5; + twin_time_t wait = _twin_idle_timeout(ctx->screen, 5); if (wait > 0) usleep((useconds_t) wait * 1000); } @@ -456,14 +446,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; @@ -482,41 +465,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_input.c b/backend/linux_input.c index 7f3f1c7b..b00e96b7 100644 --- a/backend/linux_input.c +++ b/backend/linux_input.c @@ -4,11 +4,11 @@ * All rights reserved. */ +#include #include #include #include #include -#include #include #include #include @@ -20,13 +20,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 +32,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 +50,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 +100,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 +287,90 @@ 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); - } - - /* 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) + if (poll(tm->pfds, 1 + tm->evdev_cnt, timeout_ms) <= 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); + + 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 +395,38 @@ 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); + if (tm->evdev_cnt == 0 && udev_fd < 0) { + log_error("Failed to open udev and evdev"); + twin_linux_input_destroy(tm); + return NULL; + } 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 309d4243..c7657944 100644 --- a/backend/linux_vt.h +++ b/backend/linux_vt.h @@ -13,9 +13,10 @@ #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 @@ -74,19 +75,29 @@ static inline void twin_vt_setup_signal_handler() sigaction(SIG_SWITCH_TO, &tty_sig, 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); +} + static inline bool twin_vt_setup(int *fd_ptr, struct vt_mode *old_vtm, volatile sig_atomic_t *vt_active) { /* 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 +105,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) { + 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; - } - - /* Wait until the specified virtual terminal becomes the active VT */ - if (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 +128,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; return true; + +bail: + close(*fd_ptr); + *fd_ptr = -1; + return false; } #endif diff --git a/backend/sdl.c b/backend/sdl.c index 51ff040d..898643bf 100644 --- a/backend/sdl.c +++ b/backend/sdl.c @@ -205,16 +205,6 @@ twin_context_t *twin_sdl_init(int width, int height) return NULL; } -static void twin_sdl_configure(twin_context_t *ctx) -{ - /* Screen pixels are the renderer's logical size, not the (scaled) - * window size; the pixel buffer was allocated for the former. */ - int width, height; - SDL_RenderGetLogicalSize(PRIV(ctx)->render, &width, &height); - if (width > 0 && height > 0) - twin_screen_resize(ctx->screen, width, height); -} - /* Deliver a character to the focused window */ static void _twin_sdl_ucs4(twin_screen_t *screen, twin_ucs4_t ucs4) { @@ -229,14 +219,11 @@ static bool twin_sdl_poll(twin_context_t *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 */ - if (!twin_screen_damaged(screen)) { - twin_time_t wait = _twin_timeout_delay(); - if (wait < 0 || wait > 100) - wait = 100; - if (wait > 0) - SDL_WaitEventTimeout(NULL, wait); - } + * 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; while (SDL_PollEvent(&ev)) { @@ -337,24 +324,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..0cb4cd95 100644 --- a/backend/wasm.c +++ b/backend/wasm.c @@ -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 */ @@ -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/src/api.c b/src/api.c index dfdbbbc6..8177a7dd 100644 --- a/src/api.c +++ b/src/api.c @@ -4,62 +4,26 @@ * 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) { + if (!ctx) log_error("Backend initialization failed (%dx%d)", width, height); - } 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) { @@ -84,36 +48,26 @@ 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) - */ +bool twin_dispatch_once(twin_context_t *ctx) +{ + _twin_run_timeout(); + _twin_run_work(); + /* The backend sleeps here until input or the next thing to do */ + return g_twin_backend.poll(ctx); +} + void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *)) { - /* Validate context parameter */ - if (!ctx) { - log_error("NULL context passed to twin_run"); + if (!ctx) return; - } - - assert(ctx && "NULL context passed to twin_run"); - - /* Runtime check for missing start function */ - if (!g_twin_backend.start) { - log_error("Backend has no start function - main loop not running"); + 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); + while (twin_dispatch_once(ctx)) + ; } twin_screen_t *twin_context_screen(const twin_context_t *ctx) 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/timeout.c b/src/timeout.c index 9e0a1a27..6711eb3d 100644 --- a/src/timeout.c +++ b/src/timeout.c @@ -223,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 (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) { diff --git a/src/twin_private.h b/src/twin_private.h index 92395eb3..d7b53077 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1012,6 +1012,11 @@ 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) */ +twin_time_t _twin_idle_timeout(twin_screen_t *screen, twin_time_t cap); + void _twin_run_work(void); /* Event dispatch - internal use only @@ -1117,19 +1122,22 @@ 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 */ + /* 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_context_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; + /* * Window manager policy interface * From d14c8bd20b8e62f2717301e04b271b88977502df Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 22:45:13 +0800 Subject: [PATCH 21/48] Take cursor hotspots in pixels twin_screen_set_cursor() declared its hotspot as twin_fixed_t but stores and uses it as whole pixels. The demo's background path also had a doubled slash. --- apps/main.c | 2 +- include/twin.h | 4 ++-- src/screen.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/main.c b/apps/main.c index 7b596ea6..ee89e64e 100644 --- a/apps/main.c +++ b/apps/main.c @@ -115,7 +115,7 @@ static void init_demo_apps(twin_context_t *ctx) } #if defined(CONFIG_WM_DESKTOP) twin_screen_set_background(screen, - load_background(screen, ASSET_PATH "/tux.png")); + load_background(screen, ASSET_PATH "tux.png")); #endif for (int i = 0; i < apps_count(); i++) { const apps_entry_t *app = apps_at(i); diff --git a/include/twin.h b/include/twin.h index 69e4533a..e9ee7da1 100644 --- a/include/twin.h +++ b/include/twin.h @@ -838,8 +838,8 @@ void twin_screen_set_background(twin_screen_t *screen, twin_pixmap_t *pixmap); 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_coord_t twin_screen_width(const twin_screen_t *screen); twin_coord_t twin_screen_height(const twin_screen_t *screen); diff --git a/src/screen.c b/src/screen.c index 14cadd76..82e7b784 100644 --- a/src/screen.c +++ b/src/screen.c @@ -748,8 +748,8 @@ 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); From 3508f1c26977ac7b4a0b1a4d412a4683fab61552 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:00:12 +0800 Subject: [PATCH 22/48] Call the built window manager directly The window manager is a build-time choice, so the function-pointer table and the screen's pointer to it go. The built policy defines _twin_wm_* hooks that window.c, screen.c and wm.c call directly; a hook a policy has no use for is an inline no-op, so the call compiles away. The 'none' policy moves out of wm.c into wm-none.c and is built only with WM_NONE. Policy state is allocated with the screen, so starting a policy can no longer fail. A screen being destroyed sets closing, lets its policy go, then tears the windows down without it; the fallback to 'none' during teardown is gone. Fields only one policy needs are compiled out of the others: system bars and the keyboard inset exist only in mobile builds (elsewhere twin_screen_set_bars() just hides the windows and the inset is ignored), placement, maximize state and the window title only in desktop builds, and the per-pixmap shadow together with the compositor's shadow pass only with DROP_SHADOW, which none builds can now enable too. Window flags are bitfields and style and format are bytes. The rv32im library shrinks by 1.0 to 1.7 KB. Static archives are rebuilt from scratch: every policy now defines the same symbols, so a member left from another configuration would be linked. The Kconfig prompts name the policies Desktop (classic), Mobile (embedded) and None (minimal). test-lifetime destroys a screen mid transition and mid drag with destroy callbacks that act on a sibling, and CI gains a lane without shadows. --- .github/workflows/main.yml | 2 + Makefile | 3 +- configs/Kconfig | 20 ++-- docs/phone.md | 38 ++++--- include/twin.h | 5 +- mk/common.mk | 4 +- src/pixmap.c | 5 + src/screen.c | 44 +++++--- src/twin_private.h | 224 +++++++++++++++++++++++-------------- src/window.c | 71 ++++++++---- src/wm-desktop.c | 61 ++++------ src/wm-mobile.c | 51 +++------ src/wm-none.c | 84 ++++++++++++++ src/wm.c | 117 +++++-------------- tests/test-core.c | 4 + tests/test-lifetime.c | 65 +++++++++++ tests/test-wm.c | 2 + 17 files changed, 486 insertions(+), 314 deletions(-) create mode 100644 src/wm-none.c diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e153c833..5c7bf278 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,6 +115,8 @@ jobs: 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 diff --git a/Makefile b/Makefile index abee9061..f3ff3003 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,8 @@ libtwin.a_files-y = \ src/closure.c \ src/api.c -# Window manager policies ('none' is part of wm.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 diff --git a/configs/Kconfig b/configs/Kconfig index 37621ab1..2ef24d05 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -235,27 +235,27 @@ choice runs it for its whole life: there is no switching at run time. config WM_DESKTOP - bool "Desktop window manager" + bool "Desktop (classic)" help - Overlapping windows with title bars, click-to-raise, drag-to-move, + Classic desktop: overlapping windows with title bars, click-to-raise, drag-to-move, double-click maximize, edge snapping, corner resizing, iconify/restore, close, and a drop shadow under the active window. - Costs ~3-4 KB .text on ARM Cortex-M class targets. + Costs ~8 KB .text on RV32IM (title bars add src/icon.c). config WM_MOBILE - bool "Mobile window manager" + bool "Mobile (embedded)" help - Phone-style shell: application windows stack over the work area + 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" + bool "None (minimal)" help - No window manager: no decorations, every application window covers + 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. @@ -272,14 +272,14 @@ config WM_MOBILE_EVICT config DROP_SHADOW bool "Window drop shadows" - default y - depends on !WM_NONE + 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. + 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" diff --git a/docs/phone.md b/docs/phone.md index 8509a1c5..471a317b 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -16,15 +16,19 @@ when the library is built: | Policy | Behavior | Kconfig | |--------|----------|---------| -| none | 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 | 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 | 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` | +| 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. +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 */ @@ -51,13 +55,17 @@ windows, and a `WM_NONE` build shows the first one full screen. `screen->windows` lists every window bottom to top and is the stacking order for all policies. -The policy interface (`twin_wm_ops_t` in `src/twin_private.h`) has twelve -hooks: enter/leave, layout, map/unmap, decorate, dispatch, hit test, a -pointer filter for gestures, refresh (re-place the stack after a style, -opacity, system-bar, inset or screen-size change), activate (an explicit -`twin_window_activate()`) and work area (what application windows may -cover; a policy without it shows no system bars). The work area counts -only mapped bars, minus the inset set with `twin_screen_set_inset()`. +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. @@ -78,9 +86,11 @@ 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 and -the `none` policy, which a screen also falls back to while it is torn -down; `src/wm-desktop.c` and `src/wm-mobile.c` are the other two. +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 diff --git a/include/twin.h b/include/twin.h index e9ee7da1..5d35d4a5 100644 --- a/include/twin.h +++ b/include/twin.h @@ -1034,6 +1034,7 @@ 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 */ @@ -1150,7 +1151,8 @@ void twin_window_scroll(twin_window_t *window, * @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; other managers hide them. The + * 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. @@ -1166,6 +1168,7 @@ void twin_screen_set_bars(twin_screen_t *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); diff --git a/mk/common.mk b/mk/common.mk index cb476f90..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),$$^) diff --git a/src/pixmap.c b/src/pixmap.c index 96669c80..d0660cb1 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -263,6 +263,10 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, 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); @@ -275,6 +279,7 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, _twin_pixmap_damage_extents(pixmap); pixmap->shadow = s; _twin_pixmap_damage_extents(pixmap); +#endif } twin_pointer_t twin_pixmap_pointer(twin_pixmap_t *pixmap, diff --git a/src/screen.c b/src/screen.c index 82e7b784..98d24f9b 100644 --- a/src/screen.c +++ b/src/screen.c @@ -44,7 +44,9 @@ twin_screen_t *twin_screen_create(twin_coord_t width, twin_put_span_t put_span, void *closure) { - 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; @@ -61,13 +63,7 @@ twin_screen_t *twin_screen_create(twin_coord_t width, screen->scratch_buf = twin_malloc(TWIN_SCRATCH_SIZE); screen->scratch_size = screen->scratch_buf ? TWIN_SCRATCH_SIZE : 0; - /* The window manager the library was built with, for good */ - screen->wm = &_twin_wm_built; - if (screen->wm->enter && !screen->wm->enter(screen)) { - twin_free(screen->scratch_buf); - twin_free(screen); - return NULL; - } + _twin_wm_init(screen); /* Nothing has been shown yet: the first update paints everything */ twin_screen_damage(screen, 0, 0, width, height); return screen; @@ -75,11 +71,10 @@ twin_screen_t *twin_screen_create(twin_coord_t width, void twin_screen_destroy(twin_screen_t *screen) { - /* Let the policy finish animations and drop pixmaps it owns, then tear - * the windows down under 'none', which keeps no state */ - if (screen->wm->leave) - screen->wm->leave(screen); - screen->wm = &_twin_wm_none; + /* 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); @@ -285,6 +280,7 @@ static int _span_set_subtract(const twin_span_set_t *set, 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. @@ -336,9 +332,12 @@ static inline twin_rect_t _shadow_rect(const twin_pixmap_t *p) 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); @@ -348,9 +347,11 @@ twin_rect_t _twin_pixmap_extents(const twin_pixmap_t *p) 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) @@ -403,6 +404,17 @@ static void _shadow_span(twin_argb32_t *span, 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) +{ + (void) span, (void) span_left, (void) p, (void) y, (void) l, (void) r; +} +#endif /* * One layer as seen by a single scanline. Shadows appear as their own @@ -548,7 +560,7 @@ static void _composite_row(twin_screen_t *screen, l = p->x; r = p->x + p->width; } else { - if (!p->shadow.blur || !p->shadow.alpha) + if (!_twin_pixmap_shadowed(p)) break; twin_rect_t e = _twin_pixmap_extents(p); if (y < e.top || y >= e.bottom) @@ -605,7 +617,7 @@ static void _composite_row(twin_screen_t *screen, for (twin_pixmap_t *p = screen->bottom; p; p = p->up) { if (_layer_hidden(p)) continue; - if (p->shadow.blur && p->shadow.alpha) { + 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), @@ -904,7 +916,7 @@ static bool _twin_screen_dispatch(twin_screen_t *screen, twin_event_t *event) event->u.pointer.screen_y); #endif /* Window manager gestures see pointer events first */ - if (screen->wm->filter && screen->wm->filter(screen, event)) + if (_twin_wm_filter(screen, event)) return true; return _twin_screen_route(screen, event); case TwinEventKeyDown: diff --git a/src/twin_private.h b/src/twin_private.h index d7b53077..5c981927 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -94,6 +94,13 @@ typedef struct _twin_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 * @@ -124,9 +131,11 @@ struct _twin_pixmap { 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 */ - twin_shadow_t shadow; /**< Procedural drop shadow */ + 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) */ @@ -212,14 +221,15 @@ struct _twin_screen { void *closure; /**< Backend user data */ /* Window manager policy */ - const struct _twin_wm_ops *wm; /**< Active policy */ - void *wm_data; /**< Policy private state */ - twin_window_t *windows; /**< Every window, bottom to top */ - twin_window_t *bar_top; /**< System bar above the work area */ - twin_window_t *bar_bottom; /**< System bar below the work area */ - int dispatching; /**< Busy depth: dispatch, relayout */ - twin_coord_t inset_bottom; /**< Reserved above the bottom bar (IME) */ - twin_window_t *zombies; /**< Destroyed while busy, freed when idle */ + 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 */ @@ -287,25 +297,28 @@ struct _twin_window { twin_pixmap_t *pixmap; /**< Window pixmap */ /* Window properties */ - twin_window_style_t style; /**< Window style */ - twin_format_t format; /**< Pixel format of the window pixmap */ - twin_rect_t request; /**< Requested client geometry (screen) */ - twin_rect_t client; /**< Client area rectangle (pixmap) */ + 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; /**< Active window flag */ - bool client_grab; /**< Mouse grab state */ - bool draw_queued; /**< Draw pending flag */ - bool opaque; /**< Client area is fully painted by draw() */ - bool mapped; /**< Shown by the application */ - bool frame_dirty; /**< Decorations need repainting */ - bool dead; /**< Destroyed; the struct outlives a busy screen */ - bool wm_placed; /**< Policy already chose a position (desktop) */ - bool wm_maximized; /**< Covers the work area; wm_saved is the request */ - twin_rect_t wm_saved; /**< Request to restore after maximize */ + 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 */ @@ -315,7 +328,6 @@ struct _twin_window { /* Window data */ void *client_data; /**< User data pointer */ - char *name; /**< Window title */ /* Window callbacks */ twin_draw_func_t draw; /**< Draw callback */ @@ -1142,71 +1154,102 @@ extern const twin_backend_t g_twin_backend; * Window manager policy interface * * A policy decides window geometry, decorations, stacking, and how pointer - * events reach clients. window.c owns the window objects and calls into the - * active policy at a handful of well-defined points; policies never free a - * window. The build selects one policy (_twin_wm_built); the 'none' - * policy (wm.c) is always present. + * 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. */ -typedef struct _twin_wm_ops { - /* Policy becomes active when the screen is created, and inactive when - * it is destroyed. enter() returns false when it cannot start (out of - * memory for its state); the screen is then not created. */ - bool (*enter)(twin_screen_t *screen); - void (*leave)(twin_screen_t *screen); - - /* Compute pixmap geometry (screen coordinates) and decoration border */ - void (*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 (*map)(twin_window_t *window); - void (*unmap)(twin_window_t *window, bool destroying); - - /* Paint decorations into window->pixmap (NULL: none) */ - void (*decorate)(twin_window_t *window); - - /* Deliver an event targeted at a window (NULL: forward to client) */ - bool (*dispatch)(twin_window_t *window, twin_event_t *event); - - /* Pointer hit test in screen coordinates (NULL: pixmap rectangle) */ - bool (*hit)(twin_window_t *window, twin_coord_t x, twin_coord_t y); - - /* 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 (*filter)(twin_screen_t *screen, 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 (NULL: nothing - * to do). Every window has been re-laid out already. */ - void (*refresh)(twin_screen_t *screen); - - /* Explicit activation request (twin_window_activate); NULL: map() */ - void (*activate)(twin_window_t *window); - - /* Shrink @area (preset to the whole screen) to what application windows - * may cover, e.g. minus the system bars. NULL: the whole screen, and - * the policy does not show system bars, so twin_screen_set_bars() hides - * them. */ - void (*work_area)(const twin_screen_t *screen, twin_rect_t *area); -} twin_wm_ops_t; - -#if defined(CONFIG_WM_DESKTOP) && defined(CONFIG_WM_MOBILE) -#error "CONFIG_WM_DESKTOP and CONFIG_WM_MOBILE are mutually exclusive" +#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 -/* 'none' is also what a screen falls back to while it is torn down */ -extern const twin_wm_ops_t _twin_wm_none; #if defined(CONFIG_WM_DESKTOP) -extern const twin_wm_ops_t _twin_wm_desktop; -#define _twin_wm_built _twin_wm_desktop -#elif defined(CONFIG_WM_MOBILE) -extern const twin_wm_ops_t _twin_wm_mobile; -#define _twin_wm_built _twin_wm_mobile +#define TWIN_WM_DECORATES 1 /* title bars: decorate(), hit(), activate() */ +#else +#define TWIN_WM_DECORATES 0 +#endif +#if defined(CONFIG_WM_MOBILE) +#define TWIN_WM_BARS 1 /* system bars and a work area: filter(), work_area() \ + */ +#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 -#define _twin_wm_built _twin_wm_none +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 */ @@ -1214,7 +1257,16 @@ void _twin_window_relayout(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) */ diff --git a/src/window.c b/src/window.c index 4e506769..db267261 100644 --- a/src/window.c +++ b/src/window.c @@ -48,11 +48,13 @@ static void _twin_window_unlink(twin_window_t *window) } } +#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) { @@ -123,7 +125,7 @@ twin_window_t *twin_window_create(twin_screen_t *screen, window->request = (twin_rect_t) {x, x + width, y, y + height}; window->opaque = format == TWIN_RGB16; - screen->wm->layout(window, &geom, &border); + _twin_wm_layout(window, &geom, &border); window->pixmap = twin_pixmap_create(format, geom.right - geom.left, geom.bottom - geom.top); if (!window->pixmap) { @@ -163,16 +165,21 @@ void twin_window_destroy(twin_window_t *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; - screen->wm->unmap(window, true); + 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) @@ -192,15 +199,17 @@ void twin_window_destroy(twin_window_t *window) _twin_screen_forget(screen, window->pixmap); twin_pixmap_destroy(window->pixmap); } +#if defined(CONFIG_WM_DESKTOP) twin_free(window->name); window->name = NULL; +#endif window->pixmap = NULL; window->event = NULL; window->draw = NULL; window->destroy = NULL; window->dead = true; - if (was_bar) + if (was_bar && !screen->closing) _twin_screen_relayout(screen); /* Whoever is running callbacks may still hold the pointer */ if (screen->dispatching) { @@ -214,7 +223,9 @@ void twin_window_destroy(twin_window_t *window) void twin_window_show(twin_window_t *window) { window->mapped = true; - window->screen->wm->map(window); + /* 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) @@ -222,7 +233,10 @@ void twin_window_hide(twin_window_t *window) if (!window->mapped) return; window->mapped = false; - window->screen->wm->unmap(window, false); + if (window->screen->closing) + twin_pixmap_hide(window->pixmap); + else + _twin_wm_unmap(window, false); } /* @@ -241,7 +255,9 @@ static void _twin_window_replace_pixmap(twin_window_t *window, fresh->window = window; fresh->opacity = old->opacity; +#if defined(CONFIG_DROP_SHADOW) fresh->shadow = old->shadow; +#endif if (old->scan_height < old->height) fresh->scan_height = min(old->scan_height, fresh->height); if (old->screen) @@ -265,10 +281,11 @@ void _twin_window_relayout(twin_window_t *window) twin_rect_t geom, border; twin_pixmap_t *pixmap = window->pixmap; - /* A window being destroyed may have handed its pixmap to the policy */ - if (!pixmap) + /* A window being destroyed may have handed its pixmap to the policy; + * a closing screen lays nothing out */ + if (!pixmap || window->screen->closing) return; - window->screen->wm->layout(window, &geom, &border); + _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; @@ -328,7 +345,9 @@ void twin_window_configure(twin_window_t *window, 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); @@ -336,9 +355,9 @@ void twin_window_configure(twin_window_t *window, twin_rect_t now = twin_screen_work_area(screen); if (memcmp(&area, &now, sizeof(area))) _twin_screen_relayout(screen); - else if (window->mapped && screen->wm->refresh) + else if (window->mapped && !screen->closing) /* The style may move the window between policy layers */ - screen->wm->refresh(screen); + _twin_wm_refresh(screen); _twin_screen_idle(screen); } @@ -381,6 +400,8 @@ bool _twin_window_restore(twin_window_t *window) 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); @@ -389,10 +410,12 @@ void twin_window_set_name(twin_window_t *window, const char *name) strcpy(new_name, name); twin_free(window->name); window->name = new_name; - if (window->screen->wm->decorate) { - window->frame_dirty = true; - twin_window_queue_paint(window); - } + window->frame_dirty = true; + twin_window_queue_paint(window); +#else + (void) window; + (void) name; +#endif } void twin_window_set_opaque(twin_window_t *window, bool opaque) @@ -400,8 +423,8 @@ void twin_window_set_opaque(twin_window_t *window, bool opaque) 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->wm->refresh) - window->screen->wm->refresh(window->screen); + if (window->mapped && !window->screen->closing) + _twin_wm_refresh(window->screen); } void twin_window_draw(twin_window_t *window) @@ -414,8 +437,8 @@ void twin_window_draw(twin_window_t *window) if (window->frame_dirty) { window->frame_dirty = false; - if (window->screen->wm->decorate) { - window->screen->wm->decorate(window); + if (TWIN_WM_DECORATES) { + _twin_wm_decorate(window); _twin_window_reset_clip(window); } } @@ -583,11 +606,13 @@ void twin_window_scroll(twin_window_t *window, bool _twin_window_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y) { - if (window->screen->wm->hit) - return window->screen->wm->hit(window, x, 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 } /* Forward an event to the client, in client coordinates */ @@ -614,9 +639,7 @@ bool _twin_window_deliver(twin_window_t *window, twin_event_t *event) bool _twin_window_dispatch(twin_window_t *window, twin_event_t *event) { - if (window->screen->wm->dispatch) - return window->screen->wm->dispatch(window, event); - return _twin_window_deliver(window, event); + return _twin_wm_dispatch(window, event); } /* Move @window to the top of the stacking order */ diff --git a/src/wm-desktop.c b/src/wm-desktop.c index 6e857a14..347f1d49 100644 --- a/src/wm-desktop.c +++ b/src/wm-desktop.c @@ -54,11 +54,13 @@ typedef struct { twin_time_t click_time; } twin_desktop_t; -#define DESKTOP(screen) ((twin_desktop_t *) (screen)->wm_data) +#define DESKTOP(screen) ((twin_desktop_t *) _twin_wm_state(screen)) -static void desktop_layout(twin_window_t *window, - twin_rect_t *geom, - twin_rect_t *border) +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; @@ -148,7 +150,7 @@ static void _draw_icon(twin_pixmap_t *pixmap, twin_icon_draw(pixmap, icon, m); } -static void desktop_decorate(twin_window_t *window) +void _twin_wm_decorate(twin_window_t *window) { twin_pixmap_t *pixmap = window->pixmap; twin_title_t t; @@ -303,7 +305,7 @@ static void _place_new(twin_window_t *w) _clamp_to_screen(w); } -static void desktop_map(twin_window_t *window) +void _twin_wm_map(twin_window_t *window) { twin_screen_t *screen = window->screen; @@ -322,13 +324,13 @@ static void desktop_map(twin_window_t *window) } /* twin_window_activate(): an explicit request also restores */ -static void desktop_activate(twin_window_t *window) +void _twin_wm_activate(twin_window_t *window) { _twin_pixmap_set_scan_height(window->pixmap, window->pixmap->height); - desktop_map(window); + _twin_wm_map(window); } -static void desktop_unmap(twin_window_t *window, bool destroying) +void _twin_wm_unmap(twin_window_t *window, bool destroying) { twin_screen_t *screen = window->screen; twin_desktop_t *d = DESKTOP(screen); @@ -368,7 +370,7 @@ static bool _frame_transparent(twin_pixmap_t *p, twin_coord_t x, twin_coord_t y) } } -static bool desktop_hit(twin_window_t *window, twin_coord_t x, twin_coord_t y) +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) @@ -485,7 +487,7 @@ static bool _title_press(twin_window_t *window, twin_event_t *event) _toggle_maximize(window); /* Configure handlers may have destroyed it */ if (!window->dead) - desktop_map(window); + _twin_wm_map(window); return true; } d->click = window; @@ -517,7 +519,7 @@ static bool _resize_work(void *closure) return false; } -static bool desktop_dispatch(twin_window_t *window, twin_event_t *event) +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event) { twin_screen_t *screen = window->screen; twin_desktop_t *d = DESKTOP(screen); @@ -544,7 +546,7 @@ static bool desktop_dispatch(twin_window_t *window, twin_event_t *event) } else { d->click = NULL; } - desktop_map(window); /* raise and activate */ + _twin_wm_map(window); /* raise and activate */ if (window->dead) return true; if (in_client && !_iconified(window)) { @@ -636,7 +638,7 @@ static void _clamp_to_screen(twin_window_t *w) { twin_screen_t *screen = w->screen; twin_rect_t geom, border; - desktop_layout(w, &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), @@ -652,18 +654,14 @@ static void _clamp_to_screen(twin_window_t *w) _offset(&w->request, dx, dy); } -static bool desktop_enter(twin_screen_t *screen) +void _twin_wm_init(twin_screen_t *screen) { - twin_desktop_t *d = twin_calloc(1, sizeof(*d)); - if (!d) - return false; - screen->wm_data = d; - return true; + (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 */ -static void desktop_refresh(twin_screen_t *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) { @@ -688,24 +686,11 @@ static void desktop_refresh(twin_screen_t *screen) } } -static void desktop_leave(twin_screen_t *screen) +void _twin_wm_fini(twin_screen_t *screen) { twin_desktop_t *d = DESKTOP(screen); if (d->resize_work) twin_clear_work(d->resize_work); - twin_free(screen->wm_data); - screen->wm_data = NULL; -} - -const twin_wm_ops_t _twin_wm_desktop = { - .enter = desktop_enter, - .leave = desktop_leave, - .layout = desktop_layout, - .map = desktop_map, - .unmap = desktop_unmap, - .decorate = desktop_decorate, - .dispatch = desktop_dispatch, - .hit = desktop_hit, - .refresh = desktop_refresh, - .activate = desktop_activate, -}; + d->resize_work = NULL; + d->drag = d->resize = d->click = NULL; +} diff --git a/src/wm-mobile.c b/src/wm-mobile.c index a84f80a6..0142b145 100644 --- a/src/wm-mobile.c +++ b/src/wm-mobile.c @@ -77,16 +77,18 @@ typedef struct { bool swallow; /* a press was dropped: drop until its release */ } twin_mobile_t; -#define MOBILE(screen) ((twin_mobile_t *) (screen)->wm_data) +#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); } -static void mobile_layout(twin_window_t *window, - twin_rect_t *geom, - twin_rect_t *border) +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; @@ -145,7 +147,7 @@ static bool _fullscreen_mapped(twin_screen_t *screen) static void _home_position(twin_window_t *window) { twin_rect_t geom, border; - mobile_layout(window, &geom, &border); + _twin_wm_layout(window, &geom, &border); twin_pixmap_move(window->pixmap, geom.left, geom.top); } @@ -335,7 +337,7 @@ static void _shadow_on(twin_pixmap_t *p) #endif } -static void mobile_map(twin_window_t *window) +void _twin_wm_map(twin_window_t *window) { twin_screen_t *screen = window->screen; twin_mobile_t *m = MOBILE(screen); @@ -376,7 +378,7 @@ static void mobile_map(twin_window_t *window) _start(screen, MOBILE_PUSH, 0, PUSH_MS); } -static void mobile_unmap(twin_window_t *window, bool destroying) +void _twin_wm_unmap(twin_window_t *window, bool destroying) { twin_screen_t *screen = window->screen; twin_mobile_t *m = MOBILE(screen); @@ -480,7 +482,7 @@ static twin_time_t _edge_hold_done(twin_time_t now, void *closure) } /* Edge swipe: drag from the left edge to go back */ -static bool mobile_filter(twin_screen_t *screen, twin_event_t *event) +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; @@ -520,7 +522,7 @@ static bool mobile_filter(twin_screen_t *screen, twin_event_t *event) if (dx > EDGE_SLOP && dx > abs(dy)) { _edge_end(screen, false); if (_swipe_begin(screen, m->edge.u.pointer.screen_x)) - return mobile_filter(screen, event); + return _twin_wm_filter(screen, event); _edge_end(screen, true); return false; } @@ -553,7 +555,7 @@ static bool mobile_filter(twin_screen_t *screen, twin_event_t *event) } /* Escape the application leaves alone goes back, like the edge swipe */ -static bool mobile_dispatch(twin_window_t *window, twin_event_t *event) +bool _twin_wm_dispatch(twin_window_t *window, twin_event_t *event) { if (_twin_window_deliver(window, event) || window->dead) return true; @@ -565,24 +567,19 @@ static bool mobile_dispatch(twin_window_t *window, twin_event_t *event) return false; } -static bool mobile_enter(twin_screen_t *screen) +void _twin_wm_init(twin_screen_t *screen) { - twin_mobile_t *m = twin_calloc(1, sizeof(*m)); - if (!m) - return false; - screen->wm_data = m; screen->background_color = 0xff000000; - return true; } /* A mapped window changed style or opacity: rebuild the arrangement */ -static void mobile_refresh(twin_screen_t *screen) +void _twin_wm_refresh(twin_screen_t *screen) { _finish(screen); mobile_settle(screen); } -static void mobile_leave(twin_screen_t *screen) +void _twin_wm_fini(twin_screen_t *screen) { twin_mobile_t *m = MOBILE(screen); if (m->edge_timer) @@ -591,8 +588,8 @@ static void mobile_leave(twin_screen_t *screen) twin_tween_cancel(m->tween); if (m->orphan) twin_pixmap_destroy(m->orphan); - twin_free(m); - screen->wm_data = NULL; + /* The windows go next, without the policy */ + *m = (twin_mobile_t) {0}; } static twin_coord_t _bar_height(const twin_window_t *bar) @@ -605,20 +602,8 @@ static twin_coord_t _bar_height(const twin_window_t *bar) /* Mapped system bars take their requested heights off the top and bottom * (twin_screen_work_area() keeps the result from inverting) */ -static void mobile_work_area(const twin_screen_t *screen, twin_rect_t *area) +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); } - -const twin_wm_ops_t _twin_wm_mobile = { - .enter = mobile_enter, - .leave = mobile_leave, - .layout = mobile_layout, - .map = mobile_map, - .unmap = mobile_unmap, - .dispatch = mobile_dispatch, - .filter = mobile_filter, - .refresh = mobile_refresh, - .work_area = mobile_work_area, -}; diff --git a/src/wm-none.c b/src/wm-none.c new file mode 100644 index 00000000..f26a6bcd --- /dev/null +++ b/src/wm-none.c @@ -0,0 +1,84 @@ +/* + * 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); + 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 index b7b0e7f3..f397837a 100644 --- a/src/wm.c +++ b/src/wm.c @@ -5,100 +5,23 @@ */ /* - * Screen-wide window management services and the 'none' policy - * - * The 'none' policy is what a WM_NONE build runs, and what every screen - * falls back to while it is torn down: 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. + * 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" -static void none_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}; -} - -static void none_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); - 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); -} - -static void none_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); -} - -static bool none_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); -} - -const twin_wm_ops_t _twin_wm_none = { - .layout = none_layout, - .map = none_map, - .unmap = none_unmap, - .dispatch = none_dispatch, -}; - 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); - /* Bars belong to policies that carve a work area out for them; a bar - * shown some other way (its pixmap shown directly) goes again here */ - if (!screen->wm->work_area) { - if (screen->bar_top && screen->bar_top->pixmap) - twin_pixmap_hide(screen->bar_top->pixmap); - if (screen->bar_bottom && screen->bar_bottom->pixmap) - twin_pixmap_hide(screen->bar_bottom->pixmap); - } - if (screen->wm->refresh) - screen->wm->refresh(screen); + _twin_wm_refresh(screen); twin_screen_enable_update(screen); _twin_screen_idle(screen); } @@ -107,6 +30,15 @@ void twin_screen_set_bars(twin_screen_t *screen, twin_window_t *top, twin_window_t *bottom) { +#if !TWIN_WM_BARS + /* Only the mobile window manager has system bars: elsewhere the + * windows are simply hidden */ + (void) screen; + if (top) + twin_window_hide(top); + if (bottom) + twin_window_hide(bottom); +#else /* One window cannot be both bars, and bars live on this screen */ if ((top && top->screen != screen) || (bottom && bottom->screen != screen)) return; @@ -127,39 +59,44 @@ void twin_screen_set_bars(twin_screen_t *screen, 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 (screen->wm->work_area) - screen->wm->work_area(screen, &area); +#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) { - const twin_wm_ops_t *wm = window->screen->wm; - if (!window->mapped || _twin_window_is_bar(window)) + if (!window->mapped || _twin_window_is_bar(window) || + window->screen->closing) return; /* Every policy raises and focuses a window it maps */ - if (wm->activate) - wm->activate(window); - else - wm->map(window); + _twin_wm_activate(window); } bool twin_window_back(twin_window_t *window) diff --git a/tests/test-core.c b/tests/test-core.c index f83c7821..ebaa0815 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -200,6 +200,7 @@ static void test_row_fallback(void) th_destroy(t); } +#if defined(CONFIG_DROP_SHADOW) static void test_shadow(void) { th_t *t = th_create(W, H); @@ -232,6 +233,7 @@ static void test_shadow(void) twin_pixmap_destroy(p); th_destroy(t); } +#endif static void test_rgb16_and_opacity(void) { @@ -1127,7 +1129,9 @@ 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(); diff --git a/tests/test-lifetime.c b/tests/test-lifetime.c index 61e69b5c..0621fb14 100644 --- a/tests/test-lifetime.c +++ b/tests/test-lifetime.c @@ -323,6 +323,70 @@ static void test_cache_collision(void) 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); + twin_window_t *c = probe_window(t, &pc, TwinWindowPlain, 5, 5); + twin_window_show(c); + /* 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(); @@ -336,6 +400,7 @@ int main(void) 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 index 5d6d80b2..cd2a1ccf 100644 --- a/tests/test-wm.c +++ b/tests/test-wm.c @@ -286,7 +286,9 @@ static void test_restore_handoff(void) 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); From 7b3e0c9f2b1838ef8203f27c0b00ae68ecb89c4f Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:12:38 +0800 Subject: [PATCH 23/48] Trim the public API to what applications use include/twin.h loses what nothing outside the library calls: the compositor-level pixmap calls (show, hide, move, opacity, shadow, update batching, clip reset), the work queue, the active-pixmap setter, font measuring internals, the cache and allocator controls, tween finishing and easing, and internal pixel typedefs. Their declarations move to twin_private.h, which backends, tests and tools already include. Dead definitions go: TwinDispatchReject, the button signal payload nobody read, the rounded-rectangle and tab widget shapes (and the widget radius and twin_path_tab() behind them), twin_cos(), and twin_context_screen(). Signatures lose parameters every caller passed the same way: twin_toplevel_create() always makes an ARGB32 application window, twin_pixmap_from_file() always decodes to ARGB32, twin_label_set() takes the text only, twin_window_back() returns nothing, and twin_run() hands its callback the screen. twin_create() installs the arrow cursor under CONFIG_CURSOR, so applications no longer set it up. The two TinyVG entry points become twin_tvg_to_pixmap(path, w, h), which applies CONFIG_TVG_MEMORY_BUDGET itself and computes its scale without floating point. twin_matrix_transform() replaces the x and y helpers, which rounded through 1/16 pixel. twin_window_damage() queues the repaint itself. Custom widgets embed their base widget and carry the application's data in the same allocation, and their handler receives the custom widget: the global list searched on every event and twin_widget_get_custom() are gone. --- apps/animation.c | 19 +-- apps/calc.c | 6 +- apps/cellphone/lock.c | 3 +- apps/cellphone/photos.c | 3 +- apps/cellphone/shell.c | 2 - apps/cellphone/ui.c | 1 - apps/clock.c | 16 +-- apps/image.c | 24 +--- apps/main.c | 13 +- apps/multi.c | 21 ++-- apps/spline.c | 31 ++--- backend/wasm.c | 8 +- docs/phone.md | 25 ++-- include/twin.h | 241 +++++++----------------------------- src/api.c | 19 +-- src/button.c | 2 - src/cursor.c | 11 +- src/image-tvg.c | 225 +++++++++++---------------------- src/image.c | 4 +- src/label.c | 14 +-- src/matrix.c | 18 ++- src/path.c | 23 ---- src/toplevel.c | 6 +- src/trig.c | 7 -- src/twin_private.h | 149 ++++++++++++++++++---- src/widget.c | 267 ++++++++-------------------------------- src/window.c | 1 + src/wm.c | 3 +- tests/test-core.c | 20 ++- tests/test-lifetime.c | 3 +- tools/perf.c | 2 +- 31 files changed, 394 insertions(+), 793 deletions(-) diff --git a/apps/animation.c b/apps/animation.c index cd9eceff..728c3c48 100644 --- a/apps/animation.c +++ b/apps/animation.c @@ -62,16 +62,10 @@ static twin_time_t _apps_animation_timeout(twin_time_t now, void *closure) 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); @@ -96,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, 0xff000000, 0, 0, 1, 1, _apps_animation_dispatch, + parent, 0xff000000, 0, 0, _apps_animation_dispatch, sizeof(apps_animation_data_t)); if (!custom) return NULL; @@ -129,12 +123,11 @@ bool apps_animation_start(twin_screen_t *screen, int x, int y) { - twin_pixmap_t *pix = twin_pixmap_from_file(path, TWIN_ARGB32); + twin_pixmap_t *pix = twin_pixmap_from_file(path); if (!pix) return false; twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, - twin_pixmap_width(pix), twin_pixmap_height(pix), name); + screen, x, y, twin_pixmap_width(pix), twin_pixmap_height(pix), name); if (!toplevel) { twin_pixmap_destroy(pix); return false; diff --git a/apps/calc.c b/apps/calc.c index 687ef30f..a8f813ef 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -94,8 +94,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) @@ -218,8 +217,7 @@ bool apps_calc_start(twin_screen_t *screen, 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; diff --git a/apps/cellphone/lock.c b/apps/cellphone/lock.c index f315e8cf..bf6fd4b5 100644 --- a/apps/cellphone/lock.c +++ b/apps/cellphone/lock.c @@ -86,7 +86,8 @@ 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_cos((twin_angle_t) l->phase); + 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) { diff --git a/apps/cellphone/photos.c b/apps/cellphone/photos.c index ff06e892..959ab63d 100644 --- a/apps/cellphone/photos.c +++ b/apps/cellphone/photos.c @@ -256,7 +256,7 @@ static twin_pixmap_t *load_jpeg(const char *dir, const char *stem) 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, TWIN_ARGB32); + twin_pixmap_t *pix = twin_pixmap_from_file(path); if (pix) return pix; } @@ -275,7 +275,6 @@ static twin_pixmap_t *load_photo(int idx, { twin_pixmap_t *pix = load_jpeg(dir, photos[idx].stem); if (pix && (twin_pixmap_width(pix) != w || twin_pixmap_height(pix) != h || - twin_pixmap_format(pix) != TWIN_ARGB32 || 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); diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 3b9cc92e..8691838f 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -142,7 +142,6 @@ static void _repaint_window(twin_window_t *w) return; twin_rect_t c = twin_window_client(w); twin_window_damage(w, c.left, c.top, c.right, c.bottom); - twin_window_queue_paint(w); } static int nav_state(void); @@ -438,7 +437,6 @@ static twin_time_t status_tick(twin_time_t now, void *closure) /* Only the time label changes */ twin_window_damage(w, PHONE_W / 2 - 30, 0, PHONE_W / 2 + 30, PHONE_STATUS_H); - twin_window_queue_paint(w); } return 1000; } diff --git a/apps/cellphone/ui.c b/apps/cellphone/ui.c index c699bfff..0c181e8d 100644 --- a/apps/cellphone/ui.c +++ b/apps/cellphone/ui.c @@ -147,7 +147,6 @@ void ui_view_invalidate(ui_view_t *view, ui_rect_t r) 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); - twin_window_queue_paint(w); } void ui_view_invalidate_all(ui_view_t *view) diff --git a/apps/clock.c b/apps/clock.c index 869a4839..50a3c39e 100644 --- a/apps/clock.c +++ b/apps/clock.c @@ -203,16 +203,9 @@ 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); @@ -235,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; @@ -259,8 +252,7 @@ bool apps_clock_start(twin_screen_t *screen, int w, int h) { - twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + 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))) { diff --git a/apps/image.c b/apps/image.c index 46b332eb..d17ca091 100644 --- a/apps/image.c +++ b/apps/image.c @@ -22,12 +22,7 @@ static inline twin_pixmap_t *_apps_image_pixmap(twin_custom_widget_t *image) 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 { @@ -66,16 +61,9 @@ static void _apps_image_paint(twin_custom_widget_t *custom) 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); @@ -126,8 +114,7 @@ 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, height, 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; @@ -158,8 +145,7 @@ static twin_custom_widget_t *apps_image_create(twin_box_t *parent, bool apps_image_start(twin_screen_t *screen, const char *name, int x, int y) { twin_toplevel_t *toplevel = - twin_toplevel_create(screen, TWIN_ARGB32, TwinWindowApplication, x, y, - APP_WIDTH, APP_HEIGHT, name); + twin_toplevel_create(screen, x, y, APP_WIDTH, APP_HEIGHT, name); if (!toplevel) return false; if (!apps_image_create(twin_toplevel_box(toplevel), diff --git a/apps/main.c b/apps/main.c index ee89e64e..b4a2db79 100644 --- a/apps/main.c +++ b/apps/main.c @@ -32,7 +32,7 @@ */ 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(); @@ -100,10 +100,8 @@ static const apps_entry_t *find_shell(void) * 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 = twin_context_screen(ctx); - const apps_entry_t *sh = find_shell(); if (sh) { if (sh->start(screen, 0, 0, sh->w, sh->h)) { @@ -153,13 +151,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(twin_context_screen(tx), cursor, hx, hy); -#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 b8cf5656..f34dc49e 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -87,16 +87,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); @@ -144,14 +137,14 @@ static bool apps_multiline_start(twin_screen_t *screen, 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( - twin_toplevel_box(toplevel), 0xffffffff, 0, 0, 1, 1, - _multiline_dispatch, sizeof(multiline_data_t)); + twin_toplevel_box(toplevel), 0xffffffff, 0, 0, _multiline_dispatch, + sizeof(multiline_data_t)); if (!custom) { twin_toplevel_destroy(toplevel); return false; @@ -475,7 +468,7 @@ 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", TWIN_ARGB32); + raw_background = twin_pixmap_from_file(ASSET_PATH "tux.png"); #endif if (!raw_background) return false; diff --git a/apps/spline.c b/apps/spline.c index 6663a0e3..aa759ff7 100644 --- a/apps/spline.c +++ b/apps/spline.c @@ -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); @@ -216,7 +208,7 @@ static twin_custom_widget_t *_apps_spline_init(twin_box_t *parent, twin_coord_t height) { twin_custom_widget_t *custom = twin_custom_widget_create( - parent, 0xffffffff, 0, height, 1, 1, _apps_spline_dispatch, + parent, 0xffffffff, 0, height, _apps_spline_dispatch, sizeof(apps_spline_data_t)); if (!custom) return NULL; @@ -259,8 +251,7 @@ bool apps_spline_start(twin_screen_t *screen, int w, int h) { - twin_toplevel_t *toplevel = twin_toplevel_create( - screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h, name); + 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, diff --git a/backend/wasm.c b/backend/wasm.c index 0cb4cd95..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) @@ -161,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 */ @@ -171,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; } @@ -181,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; diff --git a/docs/phone.md b/docs/phone.md index 471a317b..0380b718 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -158,9 +158,9 @@ buffer and hands it to the backend. 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.** `twin_pixmap_set_opacity()` applies a constant alpha at - scan-out (fades, dimming during transitions). -- **Procedural shadows.** `twin_pixmap_set_shadow()` attaches a shadow +- **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 @@ -190,8 +190,7 @@ 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 and -per path (`twin_path_set_font()`). Montserrat Medium and a 38-symbol +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`. @@ -217,9 +216,19 @@ the built-in renderer: 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_font_extents()`, `twin_path_set_font()`, -`twin_paint_path_linear()`, the coverage cache controls, tweens and -`twin_now()`. +`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 diff --git a/include/twin.h b/include/twin.h index 5d35d4a5..597adf0b 100644 --- a/include/twin.h +++ b/include/twin.h @@ -20,14 +20,11 @@ * 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 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) */ @@ -96,12 +93,6 @@ 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 * @@ -161,10 +152,7 @@ typedef struct _twin_event { 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; /** @@ -370,10 +358,7 @@ typedef void (*twin_destroy_func_t)(twin_window_t *window); */ typedef twin_time_t (*twin_timeout_proc_t)(twin_time_t now, void *closure); -typedef bool (*twin_work_proc_t)(void *closure); - typedef struct _twin_timeout twin_timeout_t; -typedef struct _twin_work twin_work_t; /* * Widgets @@ -389,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; /** @@ -402,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. @@ -413,9 +397,7 @@ typedef twin_dispatch_result_t (*twin_widget_proc_t)(twin_widget_t *widget, 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; @@ -472,15 +454,6 @@ bool twin_widget_set_callback(twin_widget_t *widget, twin_widget_proc_t callback, void *data); -/** - * 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 @@ -497,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 @@ -555,15 +528,6 @@ void twin_fill(twin_pixmap_t *dst, #define twin_fixed_div(a, b) ((twin_fixed_t) (((int64_t) (a) * 65536) / (b))) #define twin_fixed_abs(f) ((f) < 0 ? -(f) : (f)) -/** - * 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 UTF-8 string outline to path * @path : Path to add text to @@ -573,15 +537,6 @@ bool twin_has_ucs4(twin_font_t *font, twin_ucs4_t ucs4); */ void twin_path_utf8(twin_path_t *path, const char *string); -/** - * 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 UTF-8 string * @path : Path with current font settings @@ -594,7 +549,8 @@ void twin_text_metrics_utf8(twin_path_t *path, const char *string, twin_text_metrics_t *m); -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); /* * Frame-based animations, as loaded from an animated GIF. The animation @@ -618,11 +574,8 @@ twin_label_t *twin_label_create(twin_box_t *parent, 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_label_set_align(twin_label_t *label, twin_align_t align); @@ -645,34 +598,16 @@ void twin_matrix_rotate(twin_matrix_t *m, twin_angle_t a); */ /** - * Transform the X 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 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. + * 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 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); @@ -726,8 +661,6 @@ 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); - twin_path_t *twin_path_create(void); void twin_path_destroy(twin_path_t *path); @@ -781,7 +714,6 @@ void twin_pixmap_destroy(twin_pixmap_t *pixmap); twin_coord_t twin_pixmap_width(const twin_pixmap_t *pixmap); twin_coord_t twin_pixmap_height(const twin_pixmap_t *pixmap); -twin_format_t twin_pixmap_format(const twin_pixmap_t *pixmap); /** * Direct access to the pixel buffer @@ -804,16 +736,6 @@ twin_animation_t *twin_pixmap_animation(const twin_pixmap_t *pixmap); */ void twin_pixmap_set_transform(twin_pixmap_t *pixmap, const twin_matrix_t *m); -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_clip(twin_pixmap_t *pixmap, twin_coord_t left, twin_coord_t top, @@ -826,21 +748,8 @@ 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_move(twin_pixmap_t *pixmap, twin_coord_t x, twin_coord_t y); - -/* 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); - void twin_screen_set_background(twin_screen_t *screen, twin_pixmap_t *pixmap); -void twin_screen_set_cursor(twin_screen_t *screen, - twin_pixmap_t *pixmap, - twin_coord_t hotspot_x, - twin_coord_t hotspot_y); - twin_coord_t twin_screen_width(const twin_screen_t *screen); twin_coord_t twin_screen_height(const twin_screen_t *screen); @@ -880,8 +789,6 @@ void twin_clear_timeout(twin_timeout_t *timeout); */ 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, @@ -900,8 +807,6 @@ twin_box_t *twin_toplevel_box(twin_toplevel_t *toplevel); * trig.c */ -twin_fixed_t twin_cos(twin_angle_t a); - void twin_sincos(twin_angle_t a, twin_fixed_t *sin, twin_fixed_t *cos); /* @@ -916,9 +821,6 @@ void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape); /** Make @widget request the same preferred size as @source */ void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source); -/* Request widget repaint */ -void twin_widget_queue_paint(twin_widget_t *widget); - /* * Custom widget support - allows creating widgets without accessing internals * @@ -929,6 +831,14 @@ void twin_widget_queue_paint(twin_widget_t *widget); typedef struct _twin_custom_widget twin_custom_widget_t; +/** + * Event handler of a custom widget: called after the toolkit's own + * handling, and with TwinEventDestroy last, before the widget and its data + * are freed + */ +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. * @@ -936,36 +846,21 @@ typedef struct _twin_custom_widget twin_custom_widget_t; * @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); -/** - * 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. * @@ -1064,14 +959,14 @@ 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 @@ -1092,9 +987,9 @@ void twin_window_close(twin_window_t *window); * 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. Return true if the window handled it. + * edge swipe and the Escape key. */ -bool twin_window_back(twin_window_t *window); +void twin_window_back(twin_window_t *window); /** * Raise a mapped window and give it the keyboard, through the active @@ -1196,24 +1091,6 @@ twin_window_t *twin_window_next(const twin_window_t *window); * Compositing attributes */ -/** - * 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); - /* * Text * @@ -1221,9 +1098,6 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, * rasterized once per (font, size) and composited from then on. */ -/** 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); - /** * Draw UTF-8 text * @dst : Destination pixmap (clip and origin apply) @@ -1248,12 +1122,6 @@ twin_fixed_t twin_text_width(twin_font_t *font, twin_fixed_t size, const char *utf8); -/** 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); - /* * Gradients */ @@ -1295,9 +1163,7 @@ typedef struct _twin_cache_stats { size_t budget; /**< Configured budget in bytes */ } twin_cache_stats_t; -void twin_cache_set_budget(size_t bytes); void twin_cache_get_stats(twin_cache_stats_t *stats); -void twin_cache_flush(void); /* * Tweens: time-based property animation @@ -1340,33 +1206,20 @@ twin_tween_t *twin_tween_start(twin_time_t duration, /** Stop a tween without calling its step callback again */ void twin_tween_cancel(twin_tween_t *tween); -/** Jump a tween to its end state (step callback sees TWIN_FIXED_ONE) */ -void twin_tween_finish(twin_tween_t *tween); - -/** 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); - /** Monotonic time in milliseconds, the clock used by timeouts and tweens */ twin_time_t twin_now(void); -#define TWIN_WORK_REDISPLAY 0 -#define TWIN_WORK_PAINT 1 -#define TWIN_WORK_LAYOUT 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); - -twin_pixmap_t *twin_tvg_to_pixmap_scale(const char *filepath, - twin_format_t fmt, - twin_coord_t w, - twin_coord_t h); - -twin_pixmap_t *twin_tvg_to_pixmap_budget(const char *filepath, - twin_format_t fmt, - 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) @@ -1379,13 +1232,9 @@ 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); -/** The screen a context drives */ -twin_screen_t *twin_context_screen(const twin_context_t *ctx); - void twin_destroy(twin_context_t *ctx); #endif /* _TWIN_H_ */ diff --git a/src/api.c b/src/api.c index 8177a7dd..bff56eb2 100644 --- a/src/api.c +++ b/src/api.c @@ -14,8 +14,16 @@ twin_context_t *twin_create(int width, int height) twin_mem_pool_init(); twin_context_t *ctx = g_twin_backend.init(width, height); - if (!ctx) + 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; } @@ -56,7 +64,7 @@ bool twin_dispatch_once(twin_context_t *ctx) return g_twin_backend.poll(ctx); } -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 *)) { if (!ctx) return; @@ -65,12 +73,7 @@ void twin_run(twin_context_t *ctx, void (*init_callback)(twin_context_t *)) return; } if (init_callback) - init_callback(ctx); + init_callback(ctx->screen); while (twin_dispatch_once(ctx)) ; } - -twin_screen_t *twin_context_screen(const twin_context_t *ctx) -{ - return ctx->screen; -} diff --git a/src/button.c b/src/button.c index 70a26a27..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)); } 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/image-tvg.c b/src/image-tvg.c index cba49973..f6174d12 100644 --- a/src/image-tvg.c +++ b/src/image-tvg.c @@ -1209,180 +1209,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 0542032b..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); } diff --git a/src/matrix.c b/src/matrix.c index 18f09d99..6a96d368 100644 --- a/src/matrix.c +++ b/src/matrix.c @@ -141,16 +141,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_fixed_t twin_matrix_transform_x(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_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) -{ - 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/path.c b/src/path.c index 95c45719..ba8b6148 100644 --- a/src/path.c +++ b/src/path.c @@ -426,29 +426,6 @@ 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 = path->state.matrix; - - 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; diff --git a/src/toplevel.c b/src/toplevel.c index dd404ce3..4ebe8a62 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -99,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, @@ -108,8 +106,8 @@ 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; diff --git a/src/trig.c b/src/trig.c index 0cb8a31d..d952c837 100644 --- a/src/trig.c +++ b/src/trig.c @@ -7,13 +7,6 @@ #include "twin_private.h" -twin_fixed_t twin_cos(twin_angle_t a) -{ - twin_fixed_t cos_val = 0; - twin_sincos(a, NULL, &cos_val); - return cos_val; -} - 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 ^ diff --git a/src/twin_private.h b/src/twin_private.h index 5c981927..b894106a 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -16,6 +16,13 @@ #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) @@ -371,7 +378,6 @@ struct _twin_widget { 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 { @@ -413,8 +419,9 @@ struct _twin_context { * Encapsulates a base widget and allows attaching custom data. */ struct _twin_custom_widget { - twin_widget_t *widget; /**< Base widget functionality */ - void *data; /**< User-defined data */ + 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 */ }; /* @@ -606,7 +613,6 @@ typedef int64_t twin_xfixed_t; #define twin_sfixed_to_fixed(s) (((twin_fixed_t) (s)) * 4096) #define twin_fixed_to_sfixed(f) ((twin_sfixed_t) ((f) >> 12)) - #define twin_xfixed_to_fixed(x) ((twin_fixed_t) ((x) >> 16)) #define twin_fixed_to_xfixed(f) (((twin_xfixed_t) (f)) * 65536) @@ -746,7 +752,6 @@ static inline twin_xfixed_t _twin_xfixed_div_32bit(twin_xfixed_t a, #define TWIN_SFIXED_MIN (-0x7fff) #define TWIN_SFIXED_MAX (0x7fff) - #define TWIN_GFIXED_ONE (0x40) #define TWIN_XFIXED_ONE (0x100000000) @@ -789,7 +794,6 @@ static inline uint32_t _twin_sat_lanes(uint32_t x) (((t) = twin_get_8(d, i) + twin_get_8(s, i)), (twin_argb32_t) twin_sat(t) \ << (i)) - #define twin_argb32_to_rgb16(s) \ ((((s) >> 3) & 0x001f) | (((s) >> 5) & 0x07e0) | (((s) >> 8) & 0xf800)) #define twin_rgb16_to_argb32(s) \ @@ -1068,19 +1072,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); @@ -1141,7 +1136,7 @@ typedef struct twin_backend { /* 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_context_t *)); + void (*start)(twin_context_t *ctx, void (*init_callback)(twin_screen_t *)); /* Device cleanup when drawing is done */ void (*exit)(twin_context_t *ctx); @@ -1174,8 +1169,9 @@ extern const twin_backend_t g_twin_backend; #define TWIN_WM_DECORATES 0 #endif #if defined(CONFIG_WM_MOBILE) -#define TWIN_WM_BARS 1 /* system bars and a work area: filter(), work_area() \ - */ +#define TWIN_WM_BARS \ + 1 /* system bars and a work area: filter(), work_area() \ + */ #else #define TWIN_WM_BARS 0 #endif @@ -1254,6 +1250,9 @@ static inline bool _twin_wm_filter(twin_screen_t *screen, twin_event_t *event) /* 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); @@ -1464,14 +1463,6 @@ void twin_path_lozenge(twin_path_t *path, 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); - /** * Add filled outline of Unicode character to path * @path : Path to add character to @@ -1622,6 +1613,13 @@ 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); +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 */ @@ -1650,7 +1648,6 @@ void twin_path_convolve(twin_path_t *dest, twin_path_t *pen, twin_scratch_t *scratch); - void twin_fill_path(twin_pixmap_t *pixmap, twin_path_t *path, twin_coord_t dx, @@ -1868,4 +1865,102 @@ static inline void *_twin_widget_callback_data(const twin_widget_t *w) return w->ext ? w->ext->callback_data : NULL; } +/* + * 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 */ +#define TWIN_WORK_REDISPLAY 0 +#define TWIN_WORK_PAINT 1 +#define TWIN_WORK_LAYOUT 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 4838fd06..4ce59076 100644 --- a/src/widget.c +++ b/src/widget.c @@ -7,75 +7,33 @@ #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) -{ - 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); +#include - 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; -} +#include "twin_private.h" -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 */ +static 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); - 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, widget->background, TWIN_SOURCE, 0, 0, w, h); + return; } + 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) @@ -143,7 +101,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) @@ -250,168 +207,53 @@ void twin_widget_queue_paint(twin_widget_t *widget) _twin_widget_queue_paint(widget); } -static 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) -{ - 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; -} - -/* 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; +/* 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 register_custom_widget(twin_widget_t *widget, - twin_custom_widget_t *custom, - twin_widget_proc_t user_dispatch) +static twin_dispatch_result_t _twin_custom_dispatch(twin_widget_t *widget, + twin_event_t *event, + void *closure) { - 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_custom_widget_t *custom = (twin_custom_widget_t *) 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; -} - -static void unregister_custom_widget(twin_widget_t *widget) -{ - custom_widget_map_t **prev = &custom_widget_map; - custom_widget_map_t *entry = custom_widget_map; - - 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; - } - - /* The toolkit owns the wrapper and its data once the widget is - * created; release them with the widget. */ - twin_custom_widget_t *custom = find_custom_widget(widget); - if (custom) { - twin_free(custom->data); - twin_free(custom); - } - 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)); + twin_custom_widget_t *custom = + twin_calloc(1, CUSTOM_DATA_OFFSET + data_size); if (!custom) 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); - 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; } @@ -422,11 +264,6 @@ static twin_pixmap_t *twin_widget_pixmap(twin_widget_t *widget) 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; @@ -434,23 +271,23 @@ void *twin_custom_widget_data(twin_custom_widget_t *custom) twin_coord_t twin_custom_widget_width(twin_custom_widget_t *custom) { - return custom ? twin_widget_width(custom->widget) : 0; + return custom ? twin_widget_width(&custom->widget) : 0; } twin_coord_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; } bool twin_widget_set_callback(twin_widget_t *widget, diff --git a/src/window.c b/src/window.c index db267261..40b98dae 100644 --- a/src/window.c +++ b/src/window.c @@ -487,6 +487,7 @@ void twin_window_damage(twin_window_t *window, _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) diff --git a/src/wm.c b/src/wm.c index f397837a..2d6d2724 100644 --- a/src/wm.c +++ b/src/wm.c @@ -99,7 +99,7 @@ void twin_window_activate(twin_window_t *window) _twin_wm_activate(window); } -bool twin_window_back(twin_window_t *window) +void twin_window_back(twin_window_t *window) { twin_screen_t *screen = window->screen; twin_event_t ev = {.kind = TwinEventBack}; @@ -110,7 +110,6 @@ bool twin_window_back(twin_window_t *window) if (!handled && !window->dead) twin_window_close(window); _twin_screen_idle(screen); - return handled; } void twin_screen_focus_next(twin_screen_t *screen) diff --git a/tests/test-core.c b/tests/test-core.c index ebaa0815..01bf7911 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -822,13 +822,11 @@ static twin_dispatch_result_t count_destroy(twin_widget_t *widget, return TwinDispatchContinue; } -static twin_dispatch_result_t custom_handler(twin_widget_t *widget, - twin_event_t *event, - void *closure) +static twin_dispatch_result_t custom_handler(twin_custom_widget_t *custom, + twin_event_t *event) { - (void) widget; + (void) custom; (void) event; - (void) closure; return TwinDispatchContinue; } @@ -839,16 +837,15 @@ static void test_widget_teardown(void) 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, TWIN_ARGB32, TwinWindowApplication, 0, 0, W, H, "t"); + 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, 1, 1, - custom_handler, 64); + twin_custom_widget_create(&top->box, 0xffffffff, 10, 10, custom_handler, + 64); (void) label; twin_toplevel_show(top); th_frame(t); @@ -1039,8 +1036,7 @@ static void test_accessors(void) twin_window_destroy(w); #if defined(CONFIG_WIDGETS) - twin_toplevel_t *top = twin_toplevel_create( - t->screen, TWIN_ARGB32, TwinWindowApplication, 0, 0, W, H, "t"); + 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, @@ -1081,7 +1077,7 @@ static twin_pixmap_t *load_bytes(const uint8_t *bytes, size_t len) return NULL; bool ok = write(fd, bytes, len) == (ssize_t) len; close(fd); - twin_pixmap_t *p = ok ? twin_pixmap_from_file(path, TWIN_ARGB32) : NULL; + twin_pixmap_t *p = ok ? twin_pixmap_from_file(path) : NULL; unlink(path); return p; } diff --git a/tests/test-lifetime.c b/tests/test-lifetime.c index 0621fb14..d4dcc9c0 100644 --- a/tests/test-lifetime.c +++ b/tests/test-lifetime.c @@ -125,8 +125,7 @@ static twin_dispatch_result_t repaint_on_destroy(twin_widget_t *widget, static void test_toplevel_destroy_repaint(void) { th_t *t = th_create(W, H); - twin_toplevel_t *top = twin_toplevel_create( - t->screen, TWIN_ARGB32, TwinWindowApplication, 0, 0, W, H, "t"); + 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); diff --git a/tools/perf.c b/tools/perf.c index 931c3937..d75ae7b7 100644 --- a/tools/perf.c +++ b/tools/perf.c @@ -637,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); From ecce94abef717f12256da10c7bfb0f7e84057ed1 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:13:23 +0800 Subject: [PATCH 24/48] Put WM feature comments above their defines An inline comment too long for the line made clang-format versions disagree on how to wrap the macro. --- src/twin_private.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/twin_private.h b/src/twin_private.h index b894106a..f6bb8e30 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1163,15 +1163,15 @@ extern const twin_backend_t g_twin_backend; "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 /* title bars: decorate(), hit(), activate() */ +#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 /* system bars and a work area: filter(), work_area() \ - */ +#define TWIN_WM_BARS 1 #else #define TWIN_WM_BARS 0 #endif From 02a92fc487f58d0b917a9da669e531f5d25330b9 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:26:05 +0800 Subject: [PATCH 25/48] Spare RV32IM per-pixel divides and float All of these keep the output bit-identical: the golden frames do not change, and the new arithmetic was checked exhaustively or against the old forms where it could not be. - Linear gradients divided in 64 bits for every pixel. One pixel to the right adds exactly gx to the dot product and one row down adds gy, so the quotient and remainder are stepped both ways; test-core compares eight directions with the per-pixel formula. Vertical gradients step per row too, and the colour lerp fits 32 bits. - The built-in renderer's over, in and in_over work on two channels per multiply with the lane helpers the compositor already had (checked exhaustively per channel and on 10^8 random pixels). - Polygon edges divided per sample row; the row's quotient and remainder are precomputed and one correction step suffices. Span coverage is added four bytes at a time. - Transformed image reads step the source position by the matrix instead of two multiplies per pixel, and blend with one multiply per axis instead of two. - Opaque layer rows are copied with memcpy on scan-out, and the A8 layer divides by 255 without a divide. - twin_fixed_div() divides in 32 bits when the dividend is small or the divisor a whole number (checked on 1.7 * 10^7 pairs); tweens and shadow ramps use it. Unscaled text skips its reciprocal, and clz is inline on RISC-V without Zbb instead of a libgcc call. - The TinyVG loader works in fixed point: coordinates and widths are 16.16 from the file's integers and F32 colours are decoded from their bits, so no soft-float code is linked; every asset renders byte for byte as before, and the object is 3.3 KB smaller on rv32. On the host, culled scan-out runs twice as fast and cached text and rounded rectangles 1.2 to 1.4 times. --- include/twin.h | 15 +++- src/anim.c | 10 ++- src/draw-builtin.c | 132 ++++++++++++--------------- src/fixed.c | 3 +- src/font.c | 14 ++- src/image-tvg.c | 218 +++++++++++++++++++++------------------------ src/path.c | 92 +++++++++++++++---- src/poly.c | 55 ++++++++++-- src/screen.c | 9 +- src/twin_private.h | 24 +++++ tests/test-core.c | 62 +++++++++++++ 11 files changed, 413 insertions(+), 221 deletions(-) diff --git a/include/twin.h b/include/twin.h index 597adf0b..a48a62b3 100644 --- a/include/twin.h +++ b/include/twin.h @@ -525,7 +525,20 @@ void twin_fill(twin_pixmap_t *dst, twin_coord_t bottom); #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) * 65536) / (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)) /** diff --git a/src/anim.c b/src/anim.c index d45de0ee..0ec78ac1 100644 --- a/src/anim.c +++ b/src/anim.c @@ -66,15 +66,17 @@ 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); - bool last = elapsed >= tween->duration; + /* 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 - value = twin_ease( - tween->ease, - (twin_fixed_t) (((int64_t) elapsed << 16) / tween->duration)); + /* 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); diff --git a/src/draw-builtin.c b/src/draw-builtin.c index 2641ffac..b88eb415 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -48,70 +48,39 @@ * 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) { - uint16_t t1, t2, t3, t4; - twin_a8_t a; - if (!src) return dst; - a = ~(src >> 24); - switch (a) { - case 0: + uint32_t a = (uint8_t) ~(src >> 24); + if (!a) 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; + 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 dst; + return _twin_sat_lanes(rb + (src & TWIN_LANES)) | + _twin_sat_lanes(ag + ((src >> 8) & TWIN_LANES)) << 8; +} + +static inline twin_argb32_t in_over(twin_argb32_t dst, + twin_argb32_t src, + twin_a8_t msk) +{ + if (!msk) + return dst; + if (msk != 0xff) + src = in(src, msk); + return over(dst, src); } static inline twin_argb32_t rgb16_to_argb32(twin_rgb16_t v) @@ -741,12 +710,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; @@ -758,11 +732,15 @@ static void twin_pixmap_read_xform_8(twin_xform_t *xform, twin_coord_t line) /* for each row in the dest line */ dy = twin_int_to_fixed(line + xform->off_y); - for (dx = twin_int_to_fixed(xform->off_x); - dx < twin_int_to_fixed(xform->off_x + 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); + 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); @@ -784,11 +762,15 @@ static void twin_pixmap_read_xform_16(twin_xform_t *xform, twin_coord_t line) /* for each row in the dest line */ dy = twin_int_to_fixed(line + xform->off_y); - for (dx = twin_int_to_fixed(xform->off_x); - dx < twin_int_to_fixed(xform->off_x + 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); + 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_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); @@ -817,11 +799,15 @@ static void twin_pixmap_read_xform_32(twin_xform_t *xform, twin_coord_t line) /* for each row in the dest line */ dy = twin_int_to_fixed(line + xform->off_y); - for (dx = twin_int_to_fixed(xform->off_x); - dx < twin_int_to_fixed(xform->off_x + 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); + 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_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); diff --git a/src/fixed.c b/src/fixed.c index bb0993f2..aa3ccb7f 100644 --- a/src/fixed.c +++ b/src/fixed.c @@ -82,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.c b/src/font.c index 88e609b5..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; diff --git a/src/image-tvg.c b/src/image-tvg.c index f6174d12..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,7 +12,11 @@ #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) \ (((uint32_t) (a) << 24) | ((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | \ @@ -195,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 */ @@ -205,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 */ @@ -253,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; @@ -261,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; @@ -358,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: { @@ -426,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) @@ -450,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); @@ -461,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) @@ -643,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"); @@ -663,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: @@ -671,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: @@ -679,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: { @@ -690,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); @@ -722,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: { @@ -736,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: @@ -754,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); @@ -822,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); } @@ -834,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; @@ -885,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)); @@ -903,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); @@ -914,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)); @@ -934,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); @@ -953,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); @@ -968,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; } @@ -996,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"); @@ -1012,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; } @@ -1022,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; @@ -1030,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; } diff --git a/src/path.c b/src/path.c index ba8b6148..b28b54a2 100644 --- a/src/path.c +++ b/src/path.c @@ -788,15 +788,61 @@ static twin_argb32_t _twin_lerp_argb(twin_argb32_t c0, 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 + (int32_t) (((int64_t) (b - a) * t + 0x8000) >> 16); + int32_t v = a + (((b - a) * t + 0x8000) >> 16); out |= (twin_argb32_t) (v & 0xff) << shift; } return out; } +/* 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; + +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)--; + } +} + +/* 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++; + } +} + +/* 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); +} + void twin_paint_path_linear(twin_pixmap_t *dst, twin_path_t *path, twin_fixed_t x0, @@ -821,17 +867,24 @@ void twin_paint_path_linear(twin_pixmap_t *dst, int64_t len2 = (gx * gx + gy * gy) >> 16; /* 16.16 */ if (gx == 0 || len2 == 0) { - /* Vertical (or degenerate): one solid color per scanline */ + /* 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_fixed_t t = 0; - if (gy) { - twin_fixed_t cy = twin_int_to_fixed(y) + TWIN_FIXED_HALF; - t = (twin_fixed_t) ((((int64_t) cy - y0) * 65536) / gy); - } twin_operand_t src = {.source_kind = TWIN_SOLID, - .u.argb = _twin_lerp_argb(c0, c1, t)}; + .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; @@ -847,17 +900,26 @@ void twin_paint_path_linear(twin_pixmap_t *dst, _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_fixed_t cy = twin_int_to_fixed(y) + TWIN_FIXED_HALF; + _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++) { - twin_fixed_t cx = - twin_int_to_fixed(vis.left + i) + TWIN_FIXED_HALF; - int64_t dot = - (((int64_t) cx - x0) * gx + ((int64_t) cy - y0) * gy) >> 16; - twin_fixed_t t = (twin_fixed_t) ((dot * 65536) / len2); - row[i] = _twin_lerp_argb(c0, c1, t); + 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); diff --git a/src/poly.c b/src/poly.c index f8f2b84c..f783c0fa 100644 --- a/src/poly.c +++ b/src/poly.c @@ -6,6 +6,7 @@ */ #include +#include #include "twin_private.h" @@ -17,6 +18,10 @@ typedef struct _twin_edge { 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; @@ -46,6 +51,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 +126,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 +146,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 +251,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 */ @@ -279,7 +324,7 @@ static void _twin_edge_fill(twin_pixmap_t *pixmap, /* step all edges */ for (a = active; a; a = a->next) - _edge_step_by(a, TWIN_POLY_STEP); + _edge_step_row(a); /* fix x sorting */ for (prev = &active; (a = *prev) && (n = a->next);) { diff --git a/src/screen.c b/src/screen.c index 98d24f9b..627375a8 100644 --- a/src/screen.c +++ b/src/screen.c @@ -31,6 +31,7 @@ */ #include +#include #include "twin_private.h" @@ -294,7 +295,7 @@ static const uint16_t *_shadow_ramp(int blur) int n = 2 * blur; for (int i = 0; i <= n; i++) { /* t in 16.16, smoothstep 3t^2 - 2t^3 */ - int64_t t = ((int64_t) i << 16) / (n ? n : 1); + 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; @@ -460,13 +461,15 @@ static void _composite_layer(twin_argb32_t *span, if (p->opacity != 0xff) _twin_screen_blend_argb32(dst, src.argb32, p->opacity, w); else if (layer->opaque_full) - _twin_argb32_source_argb32((twin_pointer_t) {.argb32 = dst}, s, w); + 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++) { - uint32_t a = (uint32_t) src.a8[i] * p->opacity / 255; + /* 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]; diff --git a/src/twin_private.h b/src/twin_private.h index f6bb8e30..ec31d8cb 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1573,6 +1573,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) { diff --git a/tests/test-core.c b/tests/test-core.c index 01bf7911..8d44181d 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -467,6 +467,67 @@ static void test_gradient(void) 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; @@ -1133,6 +1194,7 @@ int main(void) test_cache_equivalence(); test_text(); test_gradient(); + test_gradient_exact(); test_tween(); test_closure_churn(); test_scroll(); From 62fe56ca2e2f6fe4dbe277fe780303a0214911f9 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:41:47 +0800 Subject: [PATCH 26/48] Shrink resident and static memory - The screen kept a cached A8 mask for paths too big for the scratch arena, so an idle screen could hold a full-size mask forever. Such masks are now taken from the heap for the one call that needs them. - The scratch arena size is a Kconfig option (SCRATCH_SIZE, KiB); 0 always uses the heap, and the goldens match at 0 and 1 KiB. - An RGB16 mask has no alpha, so it composites as no mask at all. The 24 RGB16-mask compositing functions are gone (5 KB of rv32 text together with the mask cache), and the mask index of the table covers A8, ARGB32 and solid only. - The closure tracker was a fixed 128-entry table that refused new timeouts and work once full. It now keeps 16 packed entries inline, grows on the heap past that, and hands the heap back once drained. - The coverage cache's bucket table moves from .data to .bss, cache entries put their 64-bit key first so 32-bit targets lose the padding, log callbacks drop from 32 slots to 4, and the memory statistics inline table from 512 entries to 128. - OPTIMIZE_SIZE builds with -Os and without unwind tables. A CI lane builds the phone that way with no scratch arena, the two code paths the default lanes never take. --- .github/workflows/main.yml | 2 + Makefile | 4 ++ configs/Kconfig | 19 ++++++ scripts/gen-composite-decls.py | 13 ++-- src/cache.c | 20 ++++--- src/closure.c | 55 +++++++++++++---- src/draw-builtin.c | 106 +++++++++++---------------------- src/log.c | 2 +- src/memstats.c | 2 +- src/path.c | 34 ++--------- src/screen.c | 13 ++-- src/twin_private.h | 28 ++++----- tests/harness.c | 4 -- tests/test-core.c | 41 ++++++++++++- 14 files changed, 186 insertions(+), 157 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5c7bf278..9fa52dd1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,6 +129,8 @@ jobs: 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 diff --git a/Makefile b/Makefile index f3ff3003..7db22b8f 100644 --- a/Makefile +++ b/Makefile @@ -357,6 +357,10 @@ endif # Build system integration 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) diff --git a/configs/Kconfig b/configs/Kconfig index 2ef24d05..e5ffbc34 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 @@ -317,6 +326,16 @@ 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 of each filled path. + A path whose mask does not fit takes a heap buffer for that one + call instead, so a smaller arena trades speed on large shapes for + resident memory. 0 always uses the heap. + config CACHE_SIZE int "Coverage cache budget (KiB)" default 24 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/src/cache.c b/src/cache.c index ac952af9..d01542c4 100644 --- a/src/cache.c +++ b/src/cache.c @@ -30,11 +30,13 @@ 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, budget; + size_t bytes, peak_bytes; uint32_t entries, hits, misses, evictions; } twin_cache_t; -static twin_cache_t cache = {.budget = (size_t) CONFIG_CACHE_SIZE * 1024}; +/* 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) { @@ -103,14 +105,14 @@ twin_cache_entry_t *_twin_cache_entry_alloc(twin_coord_t width, bool _twin_cache_enabled(void) { - return cache.budget > 0; + 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) + if (!cache_budget) return NULL; for (twin_cache_entry_t *e = cache.buckets[_bucket(key)]; e; e = e->hnext) { if (e->key == key) { @@ -130,14 +132,14 @@ 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) + 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) + if (data > CONFIG_CACHE_MAX_ENTRY || size > cache_budget) return NULL; - _cache_shrink(cache.budget - size); + _cache_shrink(cache_budget - size); twin_cache_entry_t *e = _twin_cache_entry_alloc(width, height); if (!e) return NULL; @@ -157,7 +159,7 @@ twin_cache_entry_t *_twin_cache_insert(uint64_t key, void twin_cache_set_budget(size_t bytes) { - cache.budget = bytes; + cache_budget = bytes; _cache_shrink(bytes); } @@ -175,5 +177,5 @@ void twin_cache_get_stats(twin_cache_stats_t *stats) stats->entries = cache.entries; stats->bytes = cache.bytes; stats->peak_bytes = cache.peak_bytes; - stats->budget = cache.budget; + stats->budget = cache_budget; } diff --git a/src/closure.c b/src/closure.c index eccde1f3..91d3b938 100644 --- a/src/closure.c +++ b/src/closure.c @@ -35,6 +35,7 @@ * 4. Work execution: _twin_closure_is_valid() check prevents use-after-free */ +#include #include #include "twin_private.h" @@ -43,7 +44,34 @@ * before any screen are tracked. * TODO: add a mutex when threading support is added */ -twin_closure_tracker_t _twin_closure_tracker; +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) +{ + 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; +} /* * Find an entry for a given closure pointer. @@ -94,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 */ @@ -115,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; } } diff --git a/src/draw-builtin.c b/src/draw-builtin.c index b88eb415..ee1b14ea 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -124,9 +124,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 @@ -173,7 +170,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 */ @@ -296,25 +292,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, @@ -329,19 +323,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, @@ -356,19 +344,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, @@ -383,19 +365,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, @@ -413,19 +389,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, @@ -440,19 +410,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, @@ -467,19 +431,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, @@ -494,19 +452,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, @@ -526,6 +478,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, @@ -544,6 +503,11 @@ static void _twin_composite_simple(twin_pixmap_t *dst, twin_coord_t sdx, sdy; twin_source_u s; + /* RGB16 carries no alpha, so as a mask it is fully opaque: no mask */ + if (msk && msk->source_kind == TWIN_PIXMAP && + msk->u.pixmap->format == TWIN_RGB16) + msk = NULL; + dst_x += dst->origin_x; dst_y += dst->origin_y; left = dst_x; @@ -599,7 +563,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); @@ -906,7 +870,7 @@ static void _twin_composite_xform(twin_pixmap_t *dst, 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) 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/memstats.c b/src/memstats.c index cfe6cf80..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; diff --git a/src/path.c b/src/path.c index b28b54a2..523eda0e 100644 --- a/src/path.c +++ b/src/path.c @@ -702,34 +702,12 @@ static bool _twin_path_mask(twin_pixmap_t *dst, memset(pixels, 0, bytes); _twin_pixmap_init_a8(&m->hdr, w, h, stride, pixels); } else { - /* Reuse the screen's mask buffer, growing it to the high-water mark */ - twin_screen_t *screen = dst->screen; - twin_pixmap_t *mask = NULL; - if (screen && screen->mask_cache && screen->mask_cache_width >= w && - screen->mask_cache_height >= h) { - mask = screen->mask_cache; - } else { - /* Grow to the larger of old and new in each dimension, so a wide - * mask followed by a tall one does not reallocate every time */ - twin_coord_t cw = w, ch = h; - if (screen && screen->mask_cache) { - cw = max(cw, screen->mask_cache_width); - ch = max(ch, screen->mask_cache_height); - } - mask = - twin_pixmap_create(TWIN_A8, screen ? cw : w, screen ? ch : h); - if (!mask) - return false; - if (screen) { - if (screen->mask_cache) - twin_pixmap_destroy(screen->mask_cache); - screen->mask_cache = mask; - screen->mask_cache_width = cw; - screen->mask_cache_height = ch; - } else { - m->heap = mask; - } - } + /* 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); diff --git a/src/screen.c b/src/screen.c index 627375a8..78444b98 100644 --- a/src/screen.c +++ b/src/screen.c @@ -58,11 +58,14 @@ twin_screen_t *twin_screen_create(twin_coord_t width, screen->put_span = put_span; screen->closure = closure; -#ifndef TWIN_SCRATCH_SIZE -#define TWIN_SCRATCH_SIZE (16 * 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 */ @@ -85,8 +88,6 @@ void twin_screen_destroy(twin_screen_t *screen) twin_pixmap_destroy(screen->background); 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); diff --git a/src/twin_private.h b/src/twin_private.h index ec31d8cb..fb251de4 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -253,11 +253,6 @@ struct _twin_screen { 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); @@ -1359,10 +1354,10 @@ void _twin_screen_fill(twin_argb32_t *dst, twin_argb32_t color, int width); * evict least recently used entries until the budget is honored. */ 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 */ - uint64_t key; - uint32_t size; /* bytes charged to the budget */ + 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) */ @@ -1759,21 +1754,20 @@ static inline bool twin_pointer_valid(const void *ptr) * callbacks are still queued. */ -/* Maximum number of tracked closures */ -#define TWIN_MAX_CLOSURES 128 +/* 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; - /* 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 */ diff --git a/tests/harness.c b/tests/harness.c index 3fa667ef..bb666d36 100644 --- a/tests/harness.c +++ b/tests/harness.c @@ -330,10 +330,6 @@ size_t th_heap_net(const th_t *t) if (p->sublen != p->inline_sublen) retained += (size_t) p->size_sublen * sizeof(int); } - if (t->screen->mask_cache) - retained += - sizeof(twin_pixmap_t) + (size_t) t->screen->mask_cache->stride * - t->screen->mask_cache->height; retained += (size_t) t->screen->span_cache_width * sizeof(twin_argb32_t); return th_heap_raw() - retained; } diff --git a/tests/test-core.c b/tests/test-core.c index 8d44181d..cd737b05 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -630,7 +630,7 @@ 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_MAX_CLOSURES; i++) { + 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); @@ -642,6 +642,21 @@ static void test_closure_churn(void) 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; @@ -810,6 +825,29 @@ static void test_scaled_clip(void) /* A translated source transform shifts which source pixels land where, * with every renderer (pixman used to drop the translation) */ +static void test_rgb16_mask(void) +{ + /* RGB16 has no alpha: masking with it equals masking with nothing */ + twin_pixmap_t *src = twin_pixmap_create(TWIN_ARGB32, 8, 8); + twin_pixmap_t *msk = twin_pixmap_create(TWIN_RGB16, 8, 8); + twin_pixmap_t *a = twin_pixmap_create(TWIN_ARGB32, 8, 8); + twin_pixmap_t *b = twin_pixmap_create(TWIN_ARGB32, 8, 8); + twin_fill(src, 0x80ff4020, TWIN_SOURCE, 0, 0, 8, 8); + twin_fill(msk, 0xff000000, TWIN_SOURCE, 0, 0, 8, 8); + twin_fill(a, 0xff102030, TWIN_SOURCE, 0, 0, 8, 8); + twin_fill(b, 0xff102030, TWIN_SOURCE, 0, 0, 8, 8); + 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_EQ(*twin_pixmap_pointer(a, 3, 3).argb32, + *twin_pixmap_pointer(b, 3, 3).argb32, "RGB16 mask is opaque"); + twin_pixmap_destroy(src); + twin_pixmap_destroy(msk); + twin_pixmap_destroy(a); + twin_pixmap_destroy(b); +} + static void test_translated_source(void) { twin_pixmap_t *src = twin_pixmap_create(TWIN_ARGB32, 16, 16); @@ -1200,6 +1238,7 @@ int main(void) test_scroll(); test_window_damage_rects(); test_scaled_clip(); + test_rgb16_mask(); test_translated_source(); test_cleared_in_pass(); test_text_clip_overshoot(); From 38dc26f8816242fdc67ece5ca384031db69eea13 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 18 Oct 2026 23:56:58 +0800 Subject: [PATCH 27/48] Restyle the phone in the Sage visual language 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, rounded controls, and a few tactile launcher icons. It replaces Olive as the default theme; Dark and Ocean fill the same roles. - phone_theme_t names the three surfaces: page and content, a grouped surface (section labels, the settings icon column, the navigation tray, the launcher behind its tiles), and the deep bars with on_bar text. - Header bars take a start action, a centered title and short status text at the end; Contacts moves Add to the start, header buttons have no fill until pressed, and bare edges carry a dot texture, drawn through one mask, that never sits behind text. - The status bar is the deep olive bar. The navigation bar is a tray of soft keys with the current page filled in the accent. - Launcher apps sit on warm white tiles that are the whole touch target and repaint whole when pressed; app and avatar colors are muted, earthy tones. - Settings rows use single-color symbols in a grouped icon column instead of colored plates; the per-item accent is gone. - Buttons are soft fills lit slightly from the top, and the camera's mode labels are no longer all caps. docs/phone.md describes the design language; the cellphone goldens are regenerated, and every behavioral check passes unchanged apart from the moved Add button. phone-demos checks that a pressed tile matches a full repaint. --- apps/cellphone/calllog.c | 2 +- apps/cellphone/camera.c | 4 +- apps/cellphone/contacts.c | 13 +-- apps/cellphone/games.c | 4 +- apps/cellphone/home.c | 55 ++++++++---- apps/cellphone/music.c | 2 +- apps/cellphone/phone.h | 34 +++++-- apps/cellphone/photos.c | 2 +- apps/cellphone/settings.c | 84 +++++++++--------- apps/cellphone/shell.c | 176 +++++++++++++++++++++---------------- apps/cellphone/sms.c | 18 ++-- apps/cellphone/ui-paint.c | 114 +++++++++++++++++++----- docs/phone.md | 40 ++++++++- tests/golden/cellphone.txt | 112 +++++++++++------------ tests/phone-contacts.c | 2 +- tests/phone-demos.c | 18 ++++ tests/phone-settings.c | 2 +- 17 files changed, 437 insertions(+), 245 deletions(-) diff --git a/apps/cellphone/calllog.c b/apps/cellphone/calllog.c index cd82e90b..aa1556b0 100644 --- a/apps/cellphone/calllog.c +++ b/apps/cellphone/calllog.c @@ -135,7 +135,7 @@ static void calllog_paint(ui_view_t *v, twin_pixmap_t *px) ui_rect_t head = ui_rect(0, 0, PHONE_W, LIST_TOP); if (ui_intersects(head, clip)) - ui_header(px, head, v->title, l->missed[0] ? l->missed : NULL); + 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) diff --git a/apps/cellphone/camera.c b/apps/cellphone/camera.c index 22978c6d..c903c50c 100644 --- a/apps/cellphone/camera.c +++ b/apps/cellphone/camera.c @@ -333,9 +333,9 @@ static void paint_controls(camera_t *c, twin_pixmap_t *px, ui_rect_t clip) 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"); + "Photo"); ui_text_in(px, FONT_SM, vr, UI_LEFT, c->video ? MODE_ON : MODE_OFF, - "VIDEO"); + "Video"); /* Last shot: a miniature of the scene, with the shot count */ ui_rect_t t = thumb_rect(&c->view); diff --git a/apps/cellphone/contacts.c b/apps/cellphone/contacts.c index baaab4c8..a2e42cf9 100644 --- a/apps/cellphone/contacts.c +++ b/apps/cellphone/contacts.c @@ -22,7 +22,6 @@ * List */ #define LIST_TOP PHONE_HEADER_H -#define PLUS_W 40 #define FRESH_MS 1200 /* how long a newly added row stays highlighted */ typedef struct { @@ -36,7 +35,8 @@ typedef struct { static ui_rect_t plus_rect(void) { - return ui_rect(PHONE_W - PLUS_W, 0, PLUS_W, PHONE_HEADER_H); + /* 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) */ @@ -145,12 +145,13 @@ static void list_paint(ui_view_t *v, twin_pixmap_t *px) ui_rect_t hdr = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); if (ui_intersects(clip, hdr)) { - ui_header(px, hdr, "Contacts", TWIN_SYM_PLUS); + 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->primary, 0x40)); + 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->primary_dk); + (p.top + p.bottom) / 2, th->on_bar); } } } @@ -585,7 +586,7 @@ static void form_paint(ui_view_t *v, twin_pixmap_t *px) ui_rect_t hdr = ui_rect(0, 0, PHONE_W, PHONE_HEADER_H); if (ui_intersects(clip, hdr)) - ui_header(px, hdr, "New Contact", NULL); + ui_header(px, hdr, NULL, "New Contact", NULL); } /* Keyboard mode suited to the field: digits for the phone number, and a diff --git a/apps/cellphone/games.c b/apps/cellphone/games.c index 38843e89..d4dcc604 100644 --- a/apps/cellphone/games.c +++ b/apps/cellphone/games.c @@ -179,8 +179,8 @@ static void game_paint(ui_view_t *v, twin_pixmap_t *px) 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), ops[g->kind]->name, - buf); + 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)) { diff --git a/apps/cellphone/home.c b/apps/cellphone/home.c index 5cac8cd9..890d993a 100644 --- a/apps/cellphone/home.c +++ b/apps/cellphone/home.c @@ -22,7 +22,9 @@ #define PER_PAGE (COLS * ROWS) #define CELL_W (PHONE_W / COLS) #define CELL_H 74 -#define PLATE 44 +#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) @@ -55,9 +57,15 @@ static ui_rect_t cell_rect(int i) 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 + 6, PLATE, + return ui_rect(cell.left + (CELL_W - PLATE) / 2, cell.top + 9, PLATE, PLATE); } @@ -65,8 +73,8 @@ 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); - twin_argb32_t label = - phone_paint_wallpaper(px, ui_view_bounds(v), phone_wallpaper()); + 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); @@ -75,11 +83,26 @@ static void home_paint(ui_view_t *v, twin_pixmap_t *px) cell.right += shift; if (!ui_intersects(cell, clip)) continue; - ui_rect_t plate = plate_rect(cell); - phone_app_icon(px, i, plate, i == h->pressed); + 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(cell.left, plate.bottom + 2, CELL_W, 16), UI_CENTER, - label, phone_app_at(i)->name); + 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; @@ -90,7 +113,7 @@ 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(plate_rect(cell_rect(i)), x, y)) + if (ui_hit(tile_rect(cell_rect(i)), x, y)) return i; } return -1; @@ -115,17 +138,17 @@ static bool settle_step(ui_view_t *v, twin_fixed_t value) return true; } -/* Plate of @app; empty when the app's page is not shown */ -static ui_rect_t shown_plate(ui_view_t *v, int app) +/* 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 plate_rect(cell_rect(app)); + 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_plate); + ui_view_set_pressed(&h->view, &h->pressed, app, shown_tile); } static bool home_event(ui_view_t *v, const ui_event_t *e) @@ -194,13 +217,13 @@ ui_view_t *home_create(void) return &h->view; } -/* Test hook: @app's icon in home-screen content coordinates; false unless - * home is on top with that icon's page shown */ +/* 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_plate(v, app); + *out = shown_tile(v, app); return out->right > out->left; } diff --git a/apps/cellphone/music.c b/apps/cellphone/music.c index d22c6d73..0315e19b 100644 --- a/apps/cellphone/music.c +++ b/apps/cellphone/music.c @@ -772,7 +772,7 @@ static void list_paint(ui_view_t *v, twin_pixmap_t *px) if (ui_intersects(clip, hdr)) { char count[16]; snprintf(count, sizeof(count), "%d songs", PHONE_TRACK_COUNT); - ui_header(px, hdr, "Music", count); + ui_header(px, hdr, NULL, "Music", count); } if (ui_intersects(clip, mini_rect(l))) paint_mini(px, l, clip); diff --git a/apps/cellphone/phone.h b/apps/cellphone/phone.h index 840a4136..6dc5fdfb 100644 --- a/apps/cellphone/phone.h +++ b/apps/cellphone/phone.h @@ -81,13 +81,23 @@ /* * 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; - twin_argb32_t navbar, navbar_grad; + 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; @@ -464,11 +474,21 @@ bool ui_view_tween(ui_view_t *view, * keeps state (pressed, value, ...) in its view struct. */ -/* Screen header: title, optional trailing text or symbol */ -void ui_header(twin_pixmap_t *px, - ui_rect_t r, - const char *title, - const char *trailing); +/* + * 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); diff --git a/apps/cellphone/photos.c b/apps/cellphone/photos.c index 959ab63d..bb5a8f4c 100644 --- a/apps/cellphone/photos.c +++ b/apps/cellphone/photos.c @@ -375,7 +375,7 @@ static void grid_paint(ui_view_t *v, twin_pixmap_t *px) ui_fill(px, ui_view_bounds(v), th->bg); if (ui_intersects(clip, head)) - ui_header(px, head, "Photos", "6 items"); + ui_header(px, head, NULL, "Photos", "6 items"); if (ui_intersects(clip, sect)) ui_section(px, sect, "Recents"); diff --git a/apps/cellphone/settings.c b/apps/cellphone/settings.c index f2f7e020..f2bcadf0 100644 --- a/apps/cellphone/settings.c +++ b/apps/cellphone/settings.c @@ -31,6 +31,7 @@ #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 @@ -90,7 +91,6 @@ typedef struct { item_kind_t kind; int id, arg; const char *sym; - twin_argb32_t accent; const char *label; const char *sub; } item_t; @@ -224,23 +224,22 @@ static void add(page_t *s, int id, int arg, const char *sym, - twin_argb32_t accent, const char *label, const char *sub) { if (s->n >= MAX_ITEMS) return; - s->items[s->n++] = (item_t) {kind, id, arg, sym, accent, label, sub}; + 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, 0, label, NULL); + 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, 0, label, NULL); + add(s, IT_INFO, id, 0, NULL, label, NULL); } static void build(page_t *s) @@ -248,45 +247,37 @@ 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, UI_RGB(0x3b82f6), - "Display", NULL); - add(s, IT_LINK, PAGE_SOUND, 0, TWIN_SYM_VOLUME, UI_RGB(0xec4899), - "Sound", NULL); - add(s, IT_LINK, PAGE_WALLPAPER, 0, TWIN_SYM_IMAGE, UI_RGB(0xf97316), - "Wallpaper", NULL); + 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, UI_RGB(0x10b981), - "Date & time", NULL); - add(s, IT_LINK, PAGE_ABOUT, 0, TWIN_SYM_LIST, UI_RGB(0x64748b), "About", - NULL); + 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, 0, phone_theme_at(i)->name, NULL); + add(s, IT_RADIO, 0, i, NULL, phone_theme_at(i)->name, NULL); section(s, "Screen"); - add(s, IT_SLIDER, PREF_BRIGHTNESS, 0, NULL, 0, "Brightness", NULL); + 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, 0, "Volume", NULL); + add(s, IT_SLIDER, PREF_VOLUME, 0, NULL, "Volume", NULL); section(s, "Feedback"); - add(s, IT_SWITCH, PREF_VIBRATE, 0, TWIN_SYM_BELL, UI_RGB(0xef4444), - "Vibrate", NULL); - add(s, IT_SWITCH, PREF_KEYPAD, 0, TWIN_SYM_KEYBOARD, UI_RGB(0x6366f1), - "Keypad tones", NULL); + 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, 0, "Wallpapers", NULL); + add(s, IT_WALLPAPERS, 0, 0, NULL, "Wallpapers", NULL); break; case PAGE_TIME: - add(s, IT_CLOCK, 0, 0, NULL, 0, "Clock", NULL); + add(s, IT_CLOCK, 0, 0, NULL, "Clock", NULL); section(s, "Format"); - add(s, IT_SWITCH, PREF_24H, 0, TWIN_SYM_CLOCK, UI_RGB(0x10b981), - "24-hour time", NULL); + 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, UI_RGB(0x3b82f6), - "Time zone", NULL); + add(s, IT_INFO, VAL_TZ, 0, TWIN_SYM_GLOBE, "Time zone", NULL); break; case PAGE_ABOUT: section(s, "Device"); @@ -417,21 +408,22 @@ static ui_rect_t value_rect(ui_rect_t row) * Painting */ -static void paint_plate(twin_pixmap_t *px, - twin_coord_t cy, - const char *sym, - twin_argb32_t accent) +/* 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) { - ui_rect_t plate = ui_rect(10, cy - PLATE / 2, PLATE, PLATE); - ui_rrect_grad(px, plate, 7, accent, ui_darken(accent, 14)); - ui_icon(px, sym, FONT_SM, plate.left + PLATE / 2, cy, UI_RGB(0xffffff)); + 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(10, cy - PLATE / 2, PLATE, PLATE); + 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); @@ -469,14 +461,18 @@ static void paint_row(page_t *s, twin_pixmap_t *px, int i, ui_rect_t r) twin_coord_t x = 14, right = r.right - 10; char buf[24]; - ui_fill(px, r, + /* 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 (it->sym) { - paint_plate(px, cy, it->sym, it->accent); - x = 10 + PLATE + 10; - } else if (it->kind == IT_RADIO) { - paint_swatch(px, cy, it->arg); - x = 10 + PLATE + 10; + 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) { @@ -666,7 +662,7 @@ static void settings_paint(ui_view_t *v, twin_pixmap_t *px) } /* 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), + ui_header(px, ui_rect(0, 0, ui_view_width(&s->view), HEADER_H), NULL, s->view.title, NULL); } diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 8691838f..8cac61c0 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -26,67 +26,80 @@ */ static const phone_theme_t themes[] = { { - .name = "Olive", - .primary = UI_RGB(0x7a9b5e), - .primary_dk = UI_RGB(0x5d7a45), - .statusbar = UI_RGB(0xc5cdb5), - .statusbar_grad = UI_RGB(0xa8b09c), - .navbar = UI_RGB(0xa8b09c), - .navbar_grad = UI_RGB(0xc5cdb5), - .bg = UI_RGB(0xcfd7ba), - .bg_grad = UI_RGB(0xc5cdb0), - .card = UI_RGB(0xeff1e8), - .text = UI_RGB(0x222222), - .text_sec = UI_RGB(0x5d6253), - .separator = UI_RGB(0xc9cdbf), - .call_green = UI_RGB(0x7a9b5e), - .call_red = UI_RGB(0xbd4b4b), - .missed_red = UI_RGB(0xb84040), - .sms_sent = UI_RGB(0xdce8c0), - .sms_recv = UI_RGB(0xeff1e8), - .indicator = UI_RGB(0xa8b09c), + /* 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(0x42a2ff), - .primary_dk = UI_RGB(0x2080e0), + .primary = UI_RGB(0x8fb06a), + .primary_dk = UI_RGB(0x6f8f4c), .statusbar = UI_RGB(0x000000), - .statusbar_grad = UI_RGB(0x000000), - .navbar = UI_RGB(0x1a1a1a), - .navbar_grad = UI_RGB(0x1a1a1a), - .bg = UI_RGB(0x121212), - .bg_grad = UI_RGB(0x121212), - .card = UI_RGB(0x2c2c2c), - .text = UI_RGB(0xe0e0e0), - .text_sec = UI_RGB(0x9e9e9e), - .separator = UI_RGB(0x3a3a3a), - .call_green = UI_RGB(0x66d06d), - .call_red = UI_RGB(0xef534d), - .missed_red = UI_RGB(0xd3392f), - .sms_sent = UI_RGB(0x2a5e40), - .sms_recv = UI_RGB(0x383838), - .indicator = UI_RGB(0x555555), - .on_primary = UI_RGB(0xffffff), + .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(0x1e88c8), - .primary_dk = UI_RGB(0x166a9c), - .statusbar = UI_RGB(0xdcecf6), - .statusbar_grad = UI_RGB(0xbcd6e8), - .navbar = UI_RGB(0xbcd6e8), - .navbar_grad = UI_RGB(0xdcecf6), - .bg = UI_RGB(0xe6f1f8), - .bg_grad = UI_RGB(0xd2e4f0), + .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(0x5b6b78), + .text_sec = UI_RGB(0x55687a), .separator = UI_RGB(0xd5dee5), - .call_green = UI_RGB(0x2eae5f), - .call_red = UI_RGB(0xd64545), - .missed_red = UI_RGB(0xc53030), - .sms_sent = UI_RGB(0xc8e4f8), + .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), @@ -208,7 +221,8 @@ 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) { - ui_vgrad(px, r, th->bg, th->bg_grad); + /* 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); @@ -270,22 +284,22 @@ 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(0x22c55e), app_dialer_open, NULL}, - {"Contacts", TWIN_SYM_CONTACTS, NULL, UI_RGB(0x3b82f6), app_contacts_open, + {"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(0x10b981), app_sms_open, NULL}, - {"Calculator", TWIN_SYM_CALCULATOR, NULL, UI_RGB(0xf59e0b), app_calc_open, + {"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(0xec4899), app_music_open, NULL}, - {"Photos", TWIN_SYM_IMAGE, NULL, UI_RGB(0xf97316), app_photos_open, NULL}, - {"Camera", TWIN_SYM_CAMERA, NULL, UI_RGB(0x6b7280), app_camera_open, NULL}, - {"Settings", TWIN_SYM_SETTINGS, NULL, UI_RGB(0x64748b), app_settings_open, + {"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(0x8b5cf6), app_calllog_open, + {"Call Log", TWIN_SYM_HISTORY, NULL, UI_RGB(0x7a6a96), app_calllog_open, NULL}, - {"Snake", NULL, glyph_snake, UI_RGB(0x14b8a6), app_snake_open, NULL}, - {"Pong", NULL, glyph_pong, UI_RGB(0x6366f1), app_pong_open, NULL}, - {"Tetris", NULL, glyph_tetris, UI_RGB(0xef4444), app_tetris_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]))) @@ -298,8 +312,8 @@ static int n_demos; static void _list_demos(void) { static const twin_argb32_t accents[] = { - UI_RGB(0x0f766e), UI_RGB(0x78716c), UI_RGB(0xa855f7), - UI_RGB(0xd946ef), UI_RGB(0x84cc16), UI_RGB(0x0ea5e9), + 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++) { @@ -398,6 +412,12 @@ void phone_app_icon(twin_pixmap_t *px, int app, ui_rect_t plate, bool pressed) 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); @@ -414,15 +434,13 @@ static void status_draw(twin_window_t *w) 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->text); - ui_icon(px, TWIN_SYM_WIFI, FONT_SM, 34, PHONE_STATUS_H / 2, th->text); + 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->text, 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->text); - ui_fill(px, ui_rect(0, PHONE_STATUS_H - 1, PHONE_W, 1), - ui_alpha(th->text, 0x20)); + th->on_bar); } static twin_time_t status_tick(twin_time_t now, void *closure) @@ -478,7 +496,7 @@ static void nav_draw(twin_window_t *w) 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), ui_alpha(th->text, 0x20)); + 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"}; @@ -486,15 +504,17 @@ static void nav_draw(twin_window_t *w) TWIN_SYM_CONTACTS}; for (int i = 0; i < NAV_TABS; i++) { ui_rect_t r = nav_tab_rect(i); - if (i == active || i == g_nav_pressed) - ui_rrect(px, r, 6, - ui_mix(th->navbar, th->primary, - i == g_nav_pressed ? 150 : 100)); + /* 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, th->text); - ui_text(px, FONT_SM, cx, r.bottom - 5, UI_CENTER, th->text, label); + ui_icon(px, sym, FONT_NORMAL, cx, r.top + 12, fg); + ui_text(px, FONT_SM, cx, r.bottom - 5, UI_CENTER, fg, label); } } diff --git a/apps/cellphone/sms.c b/apps/cellphone/sms.c index 50dc79cd..a300b458 100644 --- a/apps/cellphone/sms.c +++ b/apps/cellphone/sms.c @@ -248,10 +248,17 @@ 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_header(px, r, "", NULL); - ui_avatar(px, 20, PHONE_HEADER_H / 2, 10, name[0], ui_avatar_color(name)); - ui_text_fit(px, FONT_HEADING, ui_rect(32, 0, PHONE_W - 40, PHONE_HEADER_H), - UI_LEFT, th->text, name); + 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); } /* @@ -545,7 +552,8 @@ static void list_paint(ui_view_t *v, twin_pixmap_t *px) 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), "Messages", NULL); + 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) diff --git a/apps/cellphone/ui-paint.c b/apps/cellphone/ui-paint.c index 183885af..a8a8dba7 100644 --- a/apps/cellphone/ui-paint.c +++ b/apps/cellphone/ui-paint.c @@ -240,13 +240,33 @@ void ui_icon(twin_pixmap_t *px, 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 (shape_path && user_path) + if (!dot_mask) + dot_mask = _dot_mask_create(); + if (shape_path && user_path && dot_mask) return true; ui_fini(); return false; @@ -258,7 +278,10 @@ void ui_fini(void) 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 */ @@ -423,34 +446,70 @@ void ui_vgrad(twin_pixmap_t *px, /* * Widgets */ -void ui_header(twin_pixmap_t *px, - ui_rect_t r, - const char *title, - const char *trailing) +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(); - ui_vgrad(px, r, th->card, ui_mix(th->card, th->bg, 96)); - ui_text_in(px, FONT_HEADING, - ui_rect(r.left + 6, r.top, ui_w(r) - 40, ui_h(r)), UI_LEFT, - th->text, title); + 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) { - ui_rect_t t = ui_rect(r.right - 40, r.top, 40, ui_h(r)); - if (_is_symbol(trailing)) - ui_icon(px, trailing, FONT_HEADING, (t.left + t.right) / 2, - (t.top + t.bottom) / 2, th->primary); - else - ui_text_in(px, FONT_SM, - ui_rect(r.left, r.top, ui_w(r) - 4, ui_h(r)), UI_RIGHT, - th->primary, 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_separator(px, r); + + 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, ui_mix(th->bg, th->text_sec, 40)); - ui_text_in(px, FONT_SM, ui_rect(r.left + 6, r.top, ui_w(r) - 6, ui_h(r)), + 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); } @@ -497,8 +556,16 @@ void ui_button(twin_pixmap_t *px, { twin_argb32_t bg, fg; _button_colors(style, pressed, &bg, &fg); - if (bg) + 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)) @@ -508,9 +575,10 @@ void ui_button(twin_pixmap_t *px, 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[] = { - 0xff3b82f6, 0xff10b981, 0xfff59e0b, 0xffec4899, - 0xff8b5cf6, 0xff14b8a6, 0xffef4444, 0xff6366f1, + 0xff5a7896, 0xff5b8c45, 0xffb5843a, 0xffa95c72, + 0xff7a6a96, 0xff4f8c7c, 0xffad6a48, 0xff646c9a, }; twin_argb32_t ui_avatar_color(const char *name) diff --git a/docs/phone.md b/docs/phone.md index 0380b718..e45aff85 100644 --- a/docs/phone.md +++ b/docs/phone.md @@ -287,7 +287,7 @@ 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 (Olive, Dark, Ocean), wallpapers, clock, app +`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 @@ -295,6 +295,44 @@ 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): diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 5a30d44b..8e5a01db 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -2,59 +2,59 @@ # Regenerate with: make check-update # config 2aae645b lock 052098fa -home 5ede29ca -calculator 749994d4 -dialer-empty 7e87b318 -dialer-number 13143cec -dialer-long 682aefe0 -dialer-incall dfa59b1d -dialer-incall-unknown bd5afaa5 -calllog 341b4359 -calllog-end 94910b17 -calllog-call 8364eedc -contacts-list 27183210 -contacts-list-scrolled 4d381d80 -contacts-detail 72a4ba61 -contacts-form-keyboard 71222ee3 -contacts-list-added b0df3666 -contacts-form-validation 9a5acb98 -contacts-form-full 7ed67600 -sms-list 4c373d97 -sms-chat d1c6c574 -sms-typing c02c14bd -sms-sent d806a778 -sms-chat-dark 6e0f9a2c -music-list cf42b3c5 -music-now-playing 19eb07ff -music-paused bd052fd3 -music-seeked b16ad7d6 -music-repeat ea55957a -music-shuffle ee978eb8 -music-now-playing-dark 5f0607dd -music-list-playing fdee372f -music-list-scrolled 28a25944 -music-list-reopened 4be6db76 -photos-grid 939d998d -photos-viewer 4825f7f3 -photos-viewer-first 819e1081 -photos-viewer-bare 218e0f56 -camera d68c4510 -camera-shot 6e9386eb -camera-video 63db382b -camera-selfie 946f8ecd -settings 131f4566 -settings-display 58065c6c -settings-sound 6b5df6cf -settings-wallpaper 7a91c018 -settings-time 5b4ffa59 -settings-display-dark 85a1f340 -settings-home-dark e8e63c74 -settings-sound-changed 54b18173 -settings-wallpaper-dusk d9c420f3 -games-paused 01480e92 -games-snake 7d15230d -games-pong d2b0caba -games-tetris 31fd1cac -games-tetris-late 4f253509 -games-tetris-revealed 29ee7bdc -home-demos 622830ba +home 73cc6095 +calculator ed7ffd2f +dialer-empty 1087de64 +dialer-number 48ccddd0 +dialer-long bc533132 +dialer-incall c27fc816 +dialer-incall-unknown c24afabe +calllog eeba1230 +calllog-end 4fdea9b6 +calllog-call a0d0bedf +contacts-list afbb0925 +contacts-list-scrolled 952eb662 +contacts-detail efd057c4 +contacts-form-keyboard 8c142034 +contacts-list-added e32bb718 +contacts-form-validation 7757d26b +contacts-form-full 0aae413f +sms-list 36514e5b +sms-chat e4714d15 +sms-typing cc559959 +sms-sent b64f1d9b +sms-chat-dark 47ae631f +music-list 533d82d2 +music-now-playing 57a7ee80 +music-paused 13836cb0 +music-seeked 26feb473 +music-repeat 701616b6 +music-shuffle e3b151ef +music-now-playing-dark 929f04cb +music-list-playing 2e0c7b57 +music-list-scrolled d1349b69 +music-list-reopened 1d431c76 +photos-grid 85d311fd +photos-viewer 362482c7 +photos-viewer-first ff9f65b5 +photos-viewer-bare 5f8f7a62 +camera 8512bb33 +camera-shot 85a28c4f +camera-video 92016fd6 +camera-selfie 4a882498 +settings b5bc56d1 +settings-display 2968bb5b +settings-sound d4d79c4a +settings-wallpaper 3cc0088e +settings-time 80cecc9c +settings-display-dark b45f5bc8 +settings-home-dark a36537f7 +settings-sound-changed 0f2006cd +settings-wallpaper-dusk 1c1ece2e +games-paused 9d59274a +games-snake 5602c5c4 +games-pong c6da2b3d +games-tetris df814a13 +games-tetris-late ab76b2b5 +games-tetris-revealed 605ce401 +home-demos da13fae9 diff --git a/tests/phone-contacts.c b/tests/phone-contacts.c index 2b8651e8..f2c045db 100644 --- a/tests/phone-contacts.c +++ b/tests/phone-contacts.c @@ -103,7 +103,7 @@ static void test_list_scroll(void) static void open_form(void) { - ph_tap(PHONE_W - 20, PHONE_HEADER_H / 2); + ph_tap(20, PHONE_HEADER_H / 2); /* add, at the header start */ CHECK(app_contacts_test_form_error() != NULL, "form opened"); } diff --git a/tests/phone-demos.c b/tests/phone-demos.c index 440c7744..a288fbd4 100644 --- a/tests/phone-demos.c +++ b/tests/phone-demos.c @@ -52,6 +52,24 @@ void test_demos(void) 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) && \ diff --git a/tests/phone-settings.c b/tests/phone-settings.c index 8df7808c..962c16eb 100644 --- a/tests/phone-settings.c +++ b/tests/phone-settings.c @@ -152,7 +152,7 @@ static void test_display(void) if (open_sub("Display")) { CHECK_EQ(app_settings_pref("brightness") < 700, 1, "brightness kept across visits"); - tap_item("Olive"); + tap_item("Sage"); CHECK_EQ(phone_theme_index(), 0, "theme restored"); } back_to_root(); From bc80af14672a898569fcfb6b43f87c37db736d2d Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:01:44 +0800 Subject: [PATCH 28/48] Give every app the registry's start signature The registry wrapped each app's start function to supply its title, the animation's asset path, or to drop a size an app ignores. The start functions now take (screen, name, x, y, w, h) themselves, so the registry points at them directly; a shell such as the phone has boot and stop functions of its own instead. The declarations and ASSET_PATH move into apps.h, replacing six one-function headers and three copies of the macro. Also drop twin_sfixed_div(), twin_dfixed_mul() and _twin_matrix_determinant(), which nothing called. --- apps/animation.c | 11 +++--- apps/apps.c | 81 ++++++------------------------------------ apps/apps.h | 29 ++++++++++----- apps/apps_animation.h | 19 ---------- apps/apps_calc.h | 20 ----------- apps/apps_clock.h | 20 ----------- apps/apps_image.h | 15 -------- apps/apps_multi.h | 20 ----------- apps/apps_spline.h | 20 ----------- apps/calc.c | 2 +- apps/cellphone/shell.c | 5 +-- apps/clock.c | 2 +- apps/image.c | 12 +++++-- apps/main.c | 11 +++--- apps/multi.c | 3 +- apps/spline.c | 2 +- src/matrix.c | 14 -------- src/twin_private.h | 28 +-------------- tests/test-demos.c | 7 ++-- 19 files changed, 64 insertions(+), 257 deletions(-) delete mode 100644 apps/apps_animation.h delete mode 100644 apps/apps_calc.h delete mode 100644 apps/apps_clock.h delete mode 100644 apps/apps_image.h delete mode 100644 apps/apps_multi.h delete mode 100644 apps/apps_spline.h diff --git a/apps/animation.c b/apps/animation.c index 728c3c48..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) @@ -117,13 +117,16 @@ static twin_custom_widget_t *apps_animation_create(twin_box_t *parent, return _apps_animation_init(parent, pix); } +/* 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); + (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( diff --git a/apps/apps.c b/apps/apps.c index bbfe15d4..796f0c86 100644 --- a/apps/apps.c +++ b/apps/apps.c @@ -6,99 +6,38 @@ #include "apps.h" -#include "apps_animation.h" -#include "apps_calc.h" -#include "apps_clock.h" -#include "apps_image.h" -#include "apps_multi.h" -#include "apps_spline.h" - #if defined(CONFIG_DEMO_CELLPHONE) #include "phone.h" #endif -#define ASSET_PATH "assets/" - -#if defined(CONFIG_DEMO_CELLPHONE) -static bool start_phone(twin_screen_t *screen, int x, int y, int w, int h) -{ - (void) x, (void) y, (void) w, (void) h; - return phone_boot(screen); -} -#endif - -#if defined(CONFIG_DEMO_CLOCK) -static bool start_clock(twin_screen_t *screen, int x, int y, int w, int h) -{ - return apps_clock_start(screen, "Clock", x, y, w, h); -} -#endif - -#if defined(CONFIG_DEMO_CALCULATOR) -static bool start_calc(twin_screen_t *screen, int x, int y, int w, int h) -{ - return apps_calc_start(screen, "Calc", x, y, w, h); -} -#endif - -#if defined(CONFIG_DEMO_SPLINE) -static bool start_spline(twin_screen_t *screen, int x, int y, int w, int h) -{ - return apps_spline_start(screen, "Spline", x, y, w, h); -} -#endif - -#if defined(CONFIG_DEMO_ANIMATION) -/* Sized by the image */ -static bool start_animation(twin_screen_t *screen, int x, int y, int w, int h) -{ - (void) w, (void) h; - return apps_animation_start(screen, "Animation", ASSET_PATH "nyancat.gif", - x, y); -} -#endif - -#if defined(CONFIG_DEMO_IMAGE) -/* Fixed size */ -static bool start_image(twin_screen_t *screen, int x, int y, int w, int h) -{ - (void) w, (void) h; - return apps_image_start(screen, "Vectors", x, y); -} -#endif - -#if defined(CONFIG_DEMO_MULTI) -static bool start_multi(twin_screen_t *screen, int x, int y, int w, int h) -{ - return apps_multi_start(screen, "Demo", x, y, w, 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, true, 0, 0, PHONE_W, PHONE_H, start_phone, + {"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, false, 100, 100, 400, 400, start_multi, NULL}, + {"Demo", TWIN_SYM_LIST, 100, 100, 400, 400, apps_multi_start, NULL, NULL}, #endif #if defined(CONFIG_DEMO_CLOCK) - {"Clock", TWIN_SYM_CLOCK, false, 10, 10, 200, 200, start_clock, NULL}, + {"Clock", TWIN_SYM_CLOCK, 10, 10, 200, 200, apps_clock_start, NULL, NULL}, #endif #if defined(CONFIG_DEMO_CALCULATOR) - {"Calc", TWIN_SYM_CALCULATOR, false, 100, 100, 200, 200, start_calc, NULL}, + {"Calc", TWIN_SYM_CALCULATOR, 100, 100, 200, 200, apps_calc_start, NULL, + NULL}, #endif #if defined(CONFIG_DEMO_SPLINE) - {"Spline", TWIN_SYM_SHUFFLE, false, 20, 20, 400, 400, start_spline, NULL}, + {"Spline", TWIN_SYM_SHUFFLE, 20, 20, 400, 400, apps_spline_start, NULL, + NULL}, #endif #if defined(CONFIG_DEMO_ANIMATION) - {"Animation", TWIN_SYM_PLAY, false, 20, 20, 0, 0, start_animation, NULL}, + {"Animation", TWIN_SYM_PLAY, 20, 20, 0, 0, apps_animation_start, NULL, + NULL}, #endif #if defined(CONFIG_DEMO_IMAGE) - {"Vectors", TWIN_SYM_GLOBE, false, 20, 20, 0, 0, start_image, NULL}, + {"Vectors", TWIN_SYM_GLOBE, 20, 20, 0, 0, apps_image_start, NULL, NULL}, #endif {0}, /* keeps the table valid when nothing is built */ }; diff --git a/apps/apps.h b/apps/apps.h index caad0f55..9e004743 100644 --- a/apps/apps.h +++ b/apps/apps.h @@ -21,20 +21,33 @@ #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 */ - /* Takes over the screen and launches the other apps itself (the phone - * shell): started alone, on a screen of its own w x h */ - bool shell; - int x, y, w, h; /* placement requested on a desktop */ - /* Open the app's windows on @screen at @x,@y, @w x @h (the policy may - * place them elsewhere); false when it could not start */ - bool (*start)(twin_screen_t *screen, int x, int y, int w, int h); - void (*stop)(void); /* tear down a shell; NULL for ordinary apps */ + 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 9363a24d..00000000 --- a/apps/apps_animation.h +++ /dev/null @@ -1,19 +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 -#include - -bool 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 a28de422..00000000 --- a/apps/apps_calc.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_CALC_H_ -#define _APPS_CALC_H_ - -#include -#include - -bool 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 0ab84d52..00000000 --- a/apps/apps_clock.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_CLOCK_H_ -#define _APPS_CLOCK_H_ - -#include -#include - -bool 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 1ea7d434..00000000 --- a/apps/apps_image.h +++ /dev/null @@ -1,15 +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 -#include - -bool 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 7598f904..00000000 --- a/apps/apps_multi.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_MULTI_H_ -#define _APPS_MULTI_H_ - -#include -#include - -bool 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 9f15add6..00000000 --- a/apps/apps_spline.h +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Twin - A Tiny Window System - * Copyright (c) 2004 Keith Packard - * All rights reserved. - */ - -#ifndef _APPS_SPLINE_H_ -#define _APPS_SPLINE_H_ - -#include -#include - -bool 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 a8f813ef..cbbab09f 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 diff --git a/apps/cellphone/shell.c b/apps/cellphone/shell.c index 8cac61c0..dea789a4 100644 --- a/apps/cellphone/shell.c +++ b/apps/cellphone/shell.c @@ -318,7 +318,7 @@ static void _list_demos(void) n_demos = 0; for (int i = 0; i < apps_count(); i++) { const apps_entry_t *e = apps_at(i); - if (e->shell) + if (e->boot) continue; if (n_demos == (int) (sizeof(demos) / sizeof(demos[0]))) break; @@ -629,7 +629,8 @@ void phone_open(int app) return; if (a->demo) { /* Its own windows: the policy stacks them over the work area */ - a->demo->start(g_screen, 0, PHONE_STATUS_H, PHONE_W, PHONE_CONTENT_H); + a->demo->start(g_screen, a->demo->name, 0, PHONE_STATUS_H, PHONE_W, + PHONE_CONTENT_H); phone_view_changed(); return; } diff --git a/apps/clock.c b/apps/clock.c index 50a3c39e..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) diff --git a/apps/image.c b/apps/image.c index d17ca091..d05c8f1c 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,7 +16,6 @@ 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 @@ -142,8 +141,15 @@ static twin_custom_widget_t *apps_image_create(twin_box_t *parent, return _apps_image_init(parent, height); } -bool 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, x, y, APP_WIDTH, APP_HEIGHT, name); if (!toplevel) diff --git a/apps/main.c b/apps/main.c index b4a2db79..50f02495 100644 --- a/apps/main.c +++ b/apps/main.c @@ -18,8 +18,6 @@ #define WIDTH 640 #define HEIGHT 480 -#define ASSET_PATH "assets/" - #if defined(CONFIG_WM_DESKTOP) /* * Load the background pixmap from storage. @@ -90,7 +88,7 @@ static void sigint_handler(int sig) static const apps_entry_t *find_shell(void) { for (int i = 0; i < apps_count(); i++) - if (apps_at(i)->shell) + if (apps_at(i)->boot) return apps_at(i); return NULL; } @@ -104,7 +102,7 @@ static void init_demo_apps(twin_screen_t *screen) { const apps_entry_t *sh = find_shell(); if (sh) { - if (sh->start(screen, 0, 0, sh->w, sh->h)) { + if (sh->boot(screen)) { shell = sh; return; } @@ -117,9 +115,10 @@ static void init_demo_apps(twin_screen_t *screen) #endif for (int i = 0; i < apps_count(); i++) { const apps_entry_t *app = apps_at(i); - if (app->shell) + if (app->boot) continue; - bool started = app->start(screen, app->x, app->y, app->w, app->h); + bool started = + app->start(screen, app->name, app->x, app->y, app->w, app->h); #if defined(CONFIG_WM_NONE) if (started) break; diff --git a/apps/multi.c b/apps/multi.c index f34dc49e..49d63a6d 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -7,10 +7,9 @@ #include #include -#include "apps_multi.h" +#include "apps.h" #define D(x) twin_double_to_fixed(x) -#define ASSET_PATH "assets/" typedef struct { twin_point_t p1, p2; diff --git a/apps/spline.c b/apps/spline.c index aa759ff7..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 diff --git a/src/matrix.c b/src/matrix.c index 6a96d368..b291515c 100644 --- a/src/matrix.c +++ b/src/matrix.c @@ -71,20 +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; -} - void twin_matrix_rotate(twin_matrix_t *m, twin_angle_t a) { twin_fixed_t c, s; diff --git a/src/twin_private.h b/src/twin_private.h index fb251de4..a7bf8ba9 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -611,33 +611,9 @@ typedef int64_t twin_xfixed_t; #define twin_xfixed_to_fixed(x) ((twin_fixed_t) ((x) >> 16)) #define twin_fixed_to_xfixed(f) (((twin_xfixed_t) (f)) * 65536) -/* - * 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 - */ - +/* 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) /* 64-bit fixed-point multiplication and division * These require 128-bit intermediate results for full precision. @@ -925,8 +901,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); - /* * Path stuff */ diff --git a/tests/test-demos.c b/tests/test-demos.c index 90ea0c66..336416a1 100644 --- a/tests/test-demos.c +++ b/tests/test-demos.c @@ -73,7 +73,8 @@ static void run(const apps_entry_t *d) for (int pass = 0; pass < 2; pass++) { if (pass) base = th_heap_net(t); - CHECK(d->start(t->screen, d->x, d->y, d->w, d->h), "%s: started", what); + 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); @@ -126,7 +127,7 @@ static void run_out_of_memory(const apps_entry_t *d) th_settle(t, 500); size_t base = th_heap_net(t); _twin_alloc_fail_next = 1; - bool ok = d->start(t->screen, d->x, d->y, d->w, d->h); + 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); @@ -147,7 +148,7 @@ 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->shell) + if (d->boot) continue; run(d); #if defined(CONFIG_MEMORY_STATS) From 37a1b8728b489d688b06d6cc4b57963e1dddc9cd Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:05:49 +0800 Subject: [PATCH 29/48] Share one core between the fbdev and DRM backends fbdev.c and drm.c each carried the same span writers, VT handling, redisplay work and idle wait. backend/linux_fb.h now holds them once: a backend embeds twin_linux_fb_t first and supplies how to map its framebuffer again after a VT switch, plus an optional flush. Fixed on the way: - Both polls spun while the VT was switched away, or after mapping the framebuffer again failed: damage that cannot be shown kept the idle timeout at zero. The wait now runs to the next timer, with a bounded retry for a failed mapping. The VT signals stay blocked outside the input wait, which unblocks them atomically with ppoll(), so a switch back signalled just before the wait still ends it. - On every return to its VT, DRM ran its whole setup again, leaking the connector, CRTC and a new dumb buffer each time. It now maps the buffer it owns and sets the CRTC again. It also restores the console's CRTC on exit, and no longer refuses to start when the CRTC shows nothing (it then uses XRGB8888). - A machine without input devices or udev failed to start; the display now runs without input. A udev monitor that reports an error or hangup is dropped instead of waking every poll. Built and checked for clean failure on a missing device; no framebuffer or DRM device was available to test on hardware. --- backend/drm.c | 551 ++++++++++++------------------------------ backend/fbdev.c | 317 +++++------------------- backend/linux_fb.h | 209 ++++++++++++++++ backend/linux_input.c | 32 ++- backend/linux_vt.h | 24 +- src/timeout.c | 2 +- src/twin_private.h | 3 +- 7 files changed, 471 insertions(+), 667 deletions(-) create mode 100644 backend/linux_fb.h diff --git a/backend/drm.c b/backend/drm.c index 58cac5b6..133e81ae 100644 --- a/backend/drm.c +++ b/backend/drm.c @@ -14,437 +14,209 @@ #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 */ - twin_linux_input_t *input; - - /* Linux virtual terminal (VT) */ - int vt_fd; - volatile sig_atomic_t vt_active; /* written by the VT signal handler */ - 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 bool get_resources(int fd, drmModeResPtr *resources) +static drmModeConnectorPtr find_connector(int fd, drmModeResPtr res) { - *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) -{ - /* 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; - } - - /* 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; + 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); } - - /* 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; MAP_FAILED tells the work loop there is no mapping */ - if (tx->fb_base != MAP_FAILED) - munmap(tx->fb_base, tx->fb_len); - tx->fb_base = MAP_FAILED; - - /* 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_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_work(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); - - /* 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; } - - /* No mapping (restoring it failed): keep the damage for the next try */ - if (!tx->vt_active && tx->fb_base != MAP_FAILED && - 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); \ - } - -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) +/* 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); + } + 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; - } + tx->fb.fb_base = MAP_FAILED; + tx->fb.remap = twin_drm_map; + tx->fb.flush = twin_drm_flush; - /* Set up signal handlers for switching TTYs */ - twin_vt_setup_signal_handler(); - - /* 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); - - + 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 */ - twin_vt_restore(tx->vt_fd, &tx->old_vtm); - 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); @@ -455,42 +227,15 @@ 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) { - twin_vt_restore(tx->vt_fd, &tx->old_vtm); - } - - 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. - */ -/* Sleep in the input poll until input arrives or there is something to - * show; input is dispatched on this thread */ -static bool twin_drm_poll(twin_context_t *ctx) -{ - twin_linux_input_poll(PRIV(ctx)->input, - (int) _twin_idle_timeout(SCREEN(ctx), -1)); - return true; -} - -/* Register the Linux DRM backend */ - const twin_backend_t g_twin_backend = { .init = twin_drm_init, - .poll = twin_drm_poll, + .poll = twin_linux_fb_poll, .exit = twin_drm_exit, }; diff --git a/backend/fbdev.c b/backend/fbdev.c index 8f9cfb38..0d4d324a 100644 --- a/backend/fbdev.c +++ b/backend/fbdev.c @@ -12,193 +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 */ - twin_linux_input_t *input; - - /* Linux virtual terminal (VT) */ - int vt_fd; - volatile sig_atomic_t vt_active; /* written by the VT signal handler */ - 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 inline bool twin_fbdev_is_rgb565(twin_fbdev_t *tx) +static bool _channels(const struct fb_var_screeninfo *v, + int r, + int rl, + int g, + int gl, + int b, + int bl) { - 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; + 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_rgb888_or_argb32(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 == 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; -} + twin_fbdev_t *tx = (twin_fbdev_t *) fb; -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 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); - } - - /* No mapping (restoring it failed): keep the damage for the next try */ - if (!tx->vt_active && tx->fb_base != MAP_FAILED && - twin_screen_damaged(screen)) - twin_screen_update(screen); - return true; } @@ -214,87 +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); - - + 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 */ - twin_vt_restore(tx->vt_fd, &tx->old_vtm); - 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: @@ -307,38 +144,16 @@ 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) { - twin_vt_restore(tx->vt_fd, &tx->old_vtm); - } - - 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. - */ -/* Sleep in the input poll until input arrives or there is something to - * show; input is dispatched on this thread */ -static bool twin_fbdev_poll(twin_context_t *ctx) -{ - twin_linux_input_poll(PRIV(ctx)->input, - (int) _twin_idle_timeout(SCREEN(ctx), -1)); - return true; -} - -/* Register the Linux framebuffer backend */ const twin_backend_t g_twin_backend = { .init = twin_fbdev_init, - .poll = twin_fbdev_poll, + .poll = twin_linux_fb_poll, .exit = twin_fbdev_exit, }; 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 b00e96b7..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 @@ -345,11 +348,30 @@ static void twin_linux_edev_open(twin_linux_input_t *tm) void twin_linux_input_poll(twin_linux_input_t *tm, int timeout_ms) { - if (poll(tm->pfds, 1 + tm->evdev_cnt, timeout_ms) <= 0) + /* 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; + } struct input_event ev[64]; for (int i = 0; i < tm->evdev_cnt; i++) { @@ -410,11 +432,9 @@ twin_linux_input_t *twin_linux_input_create(twin_screen_t *screen) tm->pfds[0] = (struct pollfd) {.fd = udev_fd, .events = POLLIN}; twin_linux_edev_open(tm); - if (tm->evdev_cnt == 0 && udev_fd < 0) { - log_error("Failed to open udev and evdev"); - twin_linux_input_destroy(tm); - return NULL; - } + /* 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; } diff --git a/backend/linux_vt.h b/backend/linux_vt.h index c7657944..3a1d0c77 100644 --- a/backend/linux_vt.h +++ b/backend/linux_vt.h @@ -22,7 +22,8 @@ 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) { @@ -51,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); @@ -59,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; } } @@ -73,6 +74,14 @@ 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 */ @@ -80,11 +89,16 @@ 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, - volatile sig_atomic_t *vt_active) + volatile sig_atomic_t *vt_away) { /* Open VT0 to inquire information */ int vt0 = twin_vt_open(0); @@ -139,7 +153,7 @@ static inline bool twin_vt_setup(int *fd_ptr, } vt_fd = fd_ptr; - is_vt_actived = vt_active; + vt_away_flag = vt_away; return true; diff --git a/src/timeout.c b/src/timeout.c index 6711eb3d..fa27c64a 100644 --- a/src/timeout.c +++ b/src/timeout.c @@ -225,7 +225,7 @@ void twin_clear_timeout(twin_timeout_t *timeout) twin_time_t _twin_idle_timeout(twin_screen_t *screen, twin_time_t cap) { - if (twin_screen_damaged(screen)) + if (screen && twin_screen_damaged(screen)) return 0; twin_time_t wait = _twin_timeout_delay(); if (cap >= 0 && (wait < 0 || wait > cap)) diff --git a/src/twin_private.h b/src/twin_private.h index a7bf8ba9..a17890ae 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -999,7 +999,8 @@ 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) */ + * @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); From fe0fd308ec0dc12788700bd4de9ade0e50b47812 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:22:04 +0800 Subject: [PATCH 30/48] Keep window order in the none policy The none policy raised a mapped window's pixmap but left the window list as it was, so focus moves that walk the list picked the wrong window after an older one was shown again. Mapping now raises the list entry too. twin_screen_set_bars() also ignores another screen's windows in every build, not only the mobile one. --- src/wm-none.c | 2 ++ src/wm.c | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wm-none.c b/src/wm-none.c index f26a6bcd..e96906b5 100644 --- a/src/wm-none.c +++ b/src/wm-none.c @@ -46,6 +46,8 @@ void _twin_wm_map(twin_window_t *window) 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 */ diff --git a/src/wm.c b/src/wm.c index 2d6d2724..8f3afb97 100644 --- a/src/wm.c +++ b/src/wm.c @@ -30,18 +30,18 @@ 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 */ - (void) screen; if (top) twin_window_hide(top); if (bottom) twin_window_hide(bottom); #else - /* One window cannot be both bars, and bars live on this screen */ - if ((top && top->screen != screen) || (bottom && bottom->screen != screen)) - return; + /* One window cannot be both bars */ if (top && top == bottom) bottom = NULL; if (top == screen->bar_top && bottom == screen->bar_bottom) From f107c07b54a1f9ea57ccbcc5d3a20996ef0fe63e Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:22:04 +0800 Subject: [PATCH 31/48] Read transformed RGB16 pixmaps from the right rows The transformed reader indexed RGB16 rows through a 32-bit pointer, so it fetched rows twice as far apart and could run past the pixmap, and it turned samples outside the pixmap into opaque black instead of transparent. test-core compares a translated RGB16 source with its ARGB32 copy. An RGB16 mask is opaque everywhere, so twin_composite() drops it before choosing the transform path, and neither path samples it. --- src/draw-builtin.c | 25 ++++++++--------- tests/test-core.c | 67 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/draw-builtin.c b/src/draw-builtin.c index ee1b14ea..7c9dbb71 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -503,11 +503,6 @@ static void _twin_composite_simple(twin_pixmap_t *dst, twin_coord_t sdx, sdy; twin_source_u s; - /* RGB16 carries no alpha, so as a mask it is fully opaque: no mask */ - if (msk && msk->source_kind == TWIN_PIXMAP && - msk->u.pixmap->format == TWIN_RGB16) - msk = NULL; - dst_x += dst->origin_x; dst_y += dst->origin_y; left = dst_x; @@ -656,13 +651,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) \ @@ -906,6 +902,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/tests/test-core.c b/tests/test-core.c index cd737b05..d53184b3 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -823,31 +823,73 @@ static void test_scaled_clip(void) twin_pixmap_destroy(b); } -/* A translated source transform shifts which source pixels land where, - * with every renderer (pixman used to drop the translation) */ +/* 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 = twin_pixmap_create(TWIN_ARGB32, 8, 8); - twin_pixmap_t *msk = twin_pixmap_create(TWIN_RGB16, 8, 8); - twin_pixmap_t *a = twin_pixmap_create(TWIN_ARGB32, 8, 8); - twin_pixmap_t *b = twin_pixmap_create(TWIN_ARGB32, 8, 8); - twin_fill(src, 0x80ff4020, TWIN_SOURCE, 0, 0, 8, 8); - twin_fill(msk, 0xff000000, TWIN_SOURCE, 0, 0, 8, 8); - twin_fill(a, 0xff102030, TWIN_SOURCE, 0, 0, 8, 8); - twin_fill(b, 0xff102030, TWIN_SOURCE, 0, 0, 8, 8); + 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_EQ(*twin_pixmap_pointer(a, 3, 3).argb32, - *twin_pixmap_pointer(b, 3, 3).argb32, "RGB16 mask is opaque"); + 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); @@ -1239,6 +1281,7 @@ int main(void) test_window_damage_rects(); test_scaled_clip(); test_rgb16_mask(); + test_rgb16_xform(); test_translated_source(); test_cleared_in_pass(); test_text_clip_overshoot(); From 73d288f85164c55e04575f4cc79d779b91a4d688 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:22:04 +0800 Subject: [PATCH 32/48] Free the screen's cursor with the screen twin_create() installs the arrow cursor, but nothing freed it, and replacing a cursor dropped the old one. The screen now owns its cursor. SDL hides its own pointer when the screen draws one. --- backend/sdl.c | 4 ++++ src/screen.c | 8 +++++++- src/twin_private.h | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/sdl.c b/backend/sdl.c index 898643bf..2f4bac06 100644 --- a/backend/sdl.c +++ b/backend/sdl.c @@ -150,6 +150,10 @@ twin_context_t *twin_sdl_init(int width, int height) 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) { diff --git a/src/screen.c b/src/screen.c index 78444b98..af40c84e 100644 --- a/src/screen.c +++ b/src/screen.c @@ -86,6 +86,9 @@ void twin_screen_destroy(twin_screen_t *screen) 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]); twin_free(screen->span_cache); @@ -769,8 +772,11 @@ void twin_screen_set_cursor(twin_screen_t *screen, { 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; diff --git a/src/twin_private.h b/src/twin_private.h index a17890ae..5b3989d3 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1609,6 +1609,7 @@ 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, From 3aef09466198703b9790fbf026eea8f49ab204f7 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:22:04 +0800 Subject: [PATCH 33/48] Reject custom widget data sizes that overflow twin_custom_widget_create() added the requested data size to the widget's own without a check; a huge size wrapped and returned an undersized block. The handler documentation now says that TwinEventDestroy reaches it before the toolkit's own handling. --- include/twin.h | 5 +++-- src/widget.c | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/include/twin.h b/include/twin.h index a48a62b3..3ff4ee72 100644 --- a/include/twin.h +++ b/include/twin.h @@ -846,8 +846,9 @@ typedef struct _twin_custom_widget twin_custom_widget_t; /** * Event handler of a custom widget: called after the toolkit's own - * handling, and with TwinEventDestroy last, before the widget and its data - * are freed + * 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 twin_dispatch_result_t ( *twin_custom_proc_t)(twin_custom_widget_t *custom, twin_event_t *event); diff --git a/src/widget.c b/src/widget.c index 4ce59076..716e06ef 100644 --- a/src/widget.c +++ b/src/widget.c @@ -239,6 +239,8 @@ twin_custom_widget_t *twin_custom_widget_create(twin_box_t *parent, twin_custom_proc_t handler, size_t data_size) { + if (data_size > SIZE_MAX - CUSTOM_DATA_OFFSET) + return NULL; twin_custom_widget_t *custom = twin_calloc(1, CUSTOM_DATA_OFFSET + data_size); if (!custom) From f131d1b27ce3925afc2ad40754e7a4c053b408ae Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:22:05 +0800 Subject: [PATCH 34/48] Keep a lifetime test mid-gesture test_screen_teardown() showed its plain sibling before the gesture, which finished the push on a phone and took the press on a desktop, so the teardown never ran mid-transition. The sibling stays unmapped. The GIF fixtures also fail the run when the temporary file cannot be written. --- tests/test-lifetime.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-lifetime.c b/tests/test-lifetime.c index d4dcc9c0..dc1567dc 100644 --- a/tests/test-lifetime.c +++ b/tests/test-lifetime.c @@ -361,8 +361,9 @@ static void test_screen_teardown(void) 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); - twin_window_show(c); /* 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); From 7c5118eec099ba68d97746542ec739d97991d61e Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 00:38:10 +0800 Subject: [PATCH 35/48] Test the TinyVG hairline pen A zero line width is drawn with a hairline pen (0.01 units, 0.1 for outlined fill paths). When the loader moved to fixed point those defaults truncated to zero and the lines vanished; no bundled asset uses a zero width, so the identical-render checks missed it. test-core loads a one-line document and counts the inked pixels. The GIF and TVG cases now share one temporary-file writer that fails the run when the fixture cannot be written. --- tests/test-core.c | 65 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/tests/test-core.c b/tests/test-core.c index d53184b3..793ec037 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -1208,17 +1208,65 @@ static void test_accessors(void) th_destroy(t); } +#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"; - int fd = mkstemp(path); - if (fd < 0) + if (!write_fixture(path, bytes, len)) return NULL; - bool ok = write(fd, bytes, len) == (ssize_t) len; - close(fd); - twin_pixmap_t *p = ok ? twin_pixmap_from_file(path) : NULL; + twin_pixmap_t *p = twin_pixmap_from_file(path); unlink(path); return p; } @@ -1250,14 +1298,12 @@ static void test_gif_malformed(void) 0, 0x80, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, ',', 0, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0x44, 0x01, 0, ';'}; - int returned = 0; for (size_t cut = 0; cut <= sizeof(good); cut++) { twin_pixmap_t *p = load_bytes(good, cut); if (p) twin_pixmap_destroy(p); - returned++; } - CHECK_EQ(returned, (int) sizeof(good) + 1, "every truncated GIF returns"); + /* Reaching here is the check: every truncated GIF returned */ } #endif @@ -1292,6 +1338,9 @@ int main(void) #endif #if defined(CONFIG_LOADER_GIF) test_gif_malformed(); +#endif +#if defined(CONFIG_LOADER_TVG) + test_tvg_hairline(); #endif test_leaks(); twin_cache_flush(); From e6df6e7ca0bad8c95573d24381a9ea5391d5f277 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:29:52 +0800 Subject: [PATCH 36/48] Blend dimmed windows straight onto the background A phone transition draws the window behind at partial opacity every frame, and that window was blended over a freshly filled background: a fill, then four multiplies and two saturations a pixel, the largest cost in the phone suite. Where such a window's own pixels are opaque and nothing lies below but the solid background, each channel is mul(c, alpha) plus mul(background, 255 - alpha). Both terms are bounded by their factors, so the sum needs no saturation and the background term is one packed constant; the opacity holds for a whole frame, so the products come from a 256-byte table rebuilt only when it changes, and the background fill is skipped. Output is bit-identical (the goldens do not change). --- src/screen-ops.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++ src/screen.c | 63 ++++++++++++++++++++++++++++++++++++++----- src/twin_private.h | 13 +++++++++ 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/screen-ops.c b/src/screen-ops.c index 54af15e6..48db9e36 100644 --- a/src/screen-ops.c +++ b/src/screen-ops.c @@ -117,3 +117,69 @@ 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 af40c84e..6a4e705f 100644 --- a/src/screen.c +++ b/src/screen.c @@ -514,14 +514,13 @@ static void _background_span(twin_screen_t *screen, } } -/* Is @p, on row @y, opaque across [l, r)? 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) +/* 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) { - if (p->opacity != 0xff || (_twin_debug & TWIN_DEBUG_NO_OCCLUSION)) - return false; const twin_rect_t *o = &p->opaque_rect; if (o->left >= o->right) return false; @@ -532,6 +531,17 @@ static bool _layer_opaque_span(const twin_pixmap_t *p, 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, @@ -652,6 +662,32 @@ static void _composite_row(twin_screen_t *screen, 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]; @@ -671,6 +707,19 @@ static void _composite_row(twin_screen_t *screen, 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); } diff --git a/src/twin_private.h b/src/twin_private.h index 5b3989d3..6a08f0e3 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1321,6 +1321,19 @@ void _twin_screen_blend_rgb16(twin_argb32_t *dst, 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); /* * Coverage cache (cache.c) From 7ae3882f4c47358e91af043b34626920bd617636 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:29:52 +0800 Subject: [PATCH 37/48] Speed up the shadow beside a sliding window The shadow beside a sliding window is a narrow strip whose alphas are the same on every row away from its ends, yet each pixel looked its coverage up in two ramps again. A piece up to 64 pixels wide now keeps its alphas while the row coverage and geometry stay the same. The blend also drops its alpha clamp: it scales the destination by k = 255 - a and adds a, and the scaled alpha is at most k, so the sum never exceeds 255. Output is bit-identical. --- src/screen.c | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/screen.c b/src/screen.c index 6a4e705f..1620a8c7 100644 --- a/src/screen.c +++ b/src/screen.c @@ -291,6 +291,8 @@ static int _span_set_subtract(const twin_span_set_t *set, * 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) @@ -364,8 +366,8 @@ 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); - uint32_t alpha = a + (ag >> 16); - return rb | (ag & 0xff) << 8 | (alpha > 255 ? 255 : alpha) << 24; + /* 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, @@ -382,13 +384,42 @@ static void _shadow_span(twin_argb32_t *span, if (!fy) return; uint32_t row_alpha = (uint32_t) p->shadow.alpha * (uint32_t) fy; + twin_argb32_t *d = span + (l - span_left); + + /* 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; + } /* 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; - twin_argb32_t *d = span + (l - span_left); for (twin_coord_t x = l; x < r;) { if (x >= flat_l && x < flat_r) { twin_coord_t end = min(r, flat_r); From 066eec79f76a2480fcd15fcfd1c6f5fb07c491c5 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:29:52 +0800 Subject: [PATCH 38/48] Inline the pixel address helper twin_pixmap_pointer() starts every span loop and was a function call, millions of times in a run of the phone suite. It is a multiply and an add, now inline in twin_private.h. --- src/pixmap.c | 11 ----------- src/twin_private.h | 13 ++++++++++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/pixmap.c b/src/pixmap.c index d0660cb1..f359d5ae 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -282,17 +282,6 @@ void twin_pixmap_set_shadow(twin_pixmap_t *pixmap, #endif } -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_enable_update(twin_pixmap_t *pixmap) { if (--pixmap->disable == 0) { diff --git a/src/twin_private.h b/src/twin_private.h index 6a08f0e3..98739fa9 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1684,9 +1684,16 @@ 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 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); From 11cc07d9c84117566dafd5759aa24ea1eb77eea3 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:04 +0800 Subject: [PATCH 39/48] Rasterize large paths in bands from one edge list A filled path whose coverage mask did not fit the scratch arena took a heap mask of its full size on every paint, 62 KB for a full-screen rounded rectangle. Such a shape is now rasterized in bands of rows that fit half the arena: the edges are built and sorted once, and the scan hands each band to the compositor as it leaves it, so the coverage matches one full-height mask. The scan itself keeps the active edges in an array ordered by x, re-sorted by one stable insertion pass per row, instead of a linked list rescanned from its head after every crossing; edges are sorted by pointer (insertion sort for small paths) rather than moving whole edges through qsort. Output is bit-identical. --- configs/Kconfig | 10 ++- src/path.c | 90 ++++++++++++++++++++++- src/poly.c | 178 +++++++++++++++++++++++++++++++++------------ src/twin_private.h | 19 +++++ 4 files changed, 242 insertions(+), 55 deletions(-) diff --git a/configs/Kconfig b/configs/Kconfig index e5ffbc34..1a2e349b 100644 --- a/configs/Kconfig +++ b/configs/Kconfig @@ -331,10 +331,12 @@ config SCRATCH_SIZE default 16 range 0 1024 help - Per-screen arena that holds the coverage mask of each filled path. - A path whose mask does not fit takes a heap buffer for that one - call instead, so a smaller arena trades speed on large shapes for - resident memory. 0 always uses the heap. + 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)" diff --git a/src/path.c b/src/path.c index 523eda0e..a5811d91 100644 --- a/src/path.c +++ b/src/path.c @@ -643,7 +643,8 @@ static bool _twin_path_mask(twin_pixmap_t *dst, twin_mask_t *m, twin_coord_t *mx, twin_coord_t *my, - twin_rect_t *vis) + twin_rect_t *vis, + bool *banded) { twin_rect_t b; twin_path_bounds(path, &b); @@ -701,6 +702,10 @@ static bool _twin_path_mask(twin_pixmap_t *dst, 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 */ @@ -727,6 +732,80 @@ static void _twin_mask_release(twin_mask_t *m, twin_scratch_t *scratch) 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, @@ -738,9 +817,14 @@ void twin_composite_path(twin_pixmap_t *dst, 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)) + 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, @@ -836,7 +920,7 @@ void twin_paint_path_linear(twin_pixmap_t *dst, twin_mask_t m; twin_coord_t mx, my; twin_rect_t vis; - if (!_twin_path_mask(dst, path, sp, &m, &mx, &my, &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}; diff --git a/src/poly.c b/src/poly.c index f783c0fa..6b9e15f0 100644 --- a/src/poly.c +++ b/src/poly.c @@ -11,7 +11,6 @@ #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; @@ -36,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; @@ -271,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_row(a); - - /* 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 { @@ -380,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/twin_private.h b/src/twin_private.h index 98739fa9..6f1da4aa 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1662,6 +1662,25 @@ void twin_fill_path(twin_pixmap_t *pixmap, 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, From 30c9f9cd9e791cd7ad436feb0faee155b8799df9 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:04 +0800 Subject: [PATCH 40/48] Flatten arcs to the tolerance the rasterizer shows An arc took eight sides per pixel of radius, rounded up to a power of two: a radius-8 corner got 32 points per quarter, its chords within 1/400 of a pixel of the curve. A chord of an n-gon strays about r pi^2 / (2 n^2), so 1/16 pixel, a quarter of the 4x4 sample spacing, needs n = pi sqrt(8 r) sides: four times fewer points for the corners and circles the phone draws, with every shape built, hulled and scanned faster. The radius is bounded by the largest row or column sum of the path matrix, so an uneven ellipse gets the sides its long axis needs. Antialiased edges move slightly (a few hundred pixels per screen, by up to 47 of 255 levels); the cellphone goldens are regenerated. --- src/path.c | 21 ++++++- tests/golden/cellphone.txt | 114 ++++++++++++++++++------------------- 2 files changed, 75 insertions(+), 60 deletions(-) diff --git a/src/path.c b/src/path.c index a5811d91..78a69134 100644 --- a/src/path.c +++ b/src/path.c @@ -173,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, @@ -192,8 +202,13 @@ void twin_path_arc(twin_path_t *path, 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; diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 8e5a01db..528ee23f 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -1,60 +1,60 @@ # Golden frame checksums for the cellphone suite. # Regenerate with: make check-update # config 2aae645b -lock 052098fa -home 73cc6095 -calculator ed7ffd2f -dialer-empty 1087de64 -dialer-number 48ccddd0 -dialer-long bc533132 -dialer-incall c27fc816 -dialer-incall-unknown c24afabe -calllog eeba1230 -calllog-end 4fdea9b6 -calllog-call a0d0bedf -contacts-list afbb0925 -contacts-list-scrolled 952eb662 -contacts-detail efd057c4 -contacts-form-keyboard 8c142034 -contacts-list-added e32bb718 -contacts-form-validation 7757d26b -contacts-form-full 0aae413f -sms-list 36514e5b -sms-chat e4714d15 -sms-typing cc559959 -sms-sent b64f1d9b -sms-chat-dark 47ae631f -music-list 533d82d2 -music-now-playing 57a7ee80 -music-paused 13836cb0 -music-seeked 26feb473 -music-repeat 701616b6 -music-shuffle e3b151ef -music-now-playing-dark 929f04cb -music-list-playing 2e0c7b57 -music-list-scrolled d1349b69 -music-list-reopened 1d431c76 -photos-grid 85d311fd -photos-viewer 362482c7 -photos-viewer-first ff9f65b5 -photos-viewer-bare 5f8f7a62 -camera 8512bb33 -camera-shot 85a28c4f -camera-video 92016fd6 -camera-selfie 4a882498 -settings b5bc56d1 -settings-display 2968bb5b -settings-sound d4d79c4a -settings-wallpaper 3cc0088e -settings-time 80cecc9c -settings-display-dark b45f5bc8 -settings-home-dark a36537f7 -settings-sound-changed 0f2006cd -settings-wallpaper-dusk 1c1ece2e -games-paused 9d59274a -games-snake 5602c5c4 -games-pong c6da2b3d -games-tetris df814a13 -games-tetris-late ab76b2b5 -games-tetris-revealed 605ce401 -home-demos da13fae9 +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 e6768493 +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 From 0f5f62679c8e07fb403d41f1bcc2f0f1ee82724f Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:14 +0800 Subject: [PATCH 41/48] Paint before redisplay and defer revealed windows The work queue ran the backend's redisplay before the paints, so whatever a pass painted reached the screen only on the next pass, after a second scan-out. Layout now runs first, then painting, then redisplay. The headless backend scanned out from its poll, right after injecting input and before that input's paints; it now scans out from redisplay work like the other backends. That also makes it safe to defer painting: a window the mobile policy reveals again is repainted from the work queue rather than at once, so closing a stack of apps never repaints the ones closed next, and the Multi demo's windows, which on a phone each cover the one before, paint only if still shown. --- apps/multi.c | 5 ++++- backend/headless.c | 24 +++++++++++------------- src/twin_private.h | 8 +++++--- src/window.c | 4 +++- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/multi.c b/apps/multi.c index 49d63a6d..f03745da 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -211,7 +211,10 @@ static bool start_window(twin_screen_t *screen, twin_window_set_name(window, name); twin_window_set_client_data(window, data); twin_window_set_callbacks(window, draw, NULL, destroy); - twin_window_draw(window); + /* 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; } diff --git a/backend/headless.c b/backend/headless.c index ebfa0adb..96a3c21c 100644 --- a/backend/headless.c +++ b/backend/headless.c @@ -216,14 +216,14 @@ static void _twin_headless_inject_events(twin_context_t *ctx) } } -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; @@ -252,6 +252,7 @@ static void twin_headless_update_damage(void *closure) tx->last_frame_time = now; tx->frame_count = 0; } + return true; } static bool twin_headless_poll(twin_context_t *ctx) @@ -267,16 +268,11 @@ static bool twin_headless_poll(twin_context_t *ctx) /* Inject events from control tool */ _twin_headless_inject_events(ctx); - /* One scan-out per iteration, after timeouts, work and input */ - if (twin_screen_damaged(ctx->screen)) - twin_headless_update_damage(ctx); - else { - /* Idle: sleep until the next timeout, but stay responsive to the - * control tool. */ - twin_time_t wait = _twin_idle_timeout(ctx->screen, 5); - if (wait > 0) - usleep((useconds_t) wait * 1000); - } + /* 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) { @@ -430,7 +426,9 @@ twin_context_t *twin_headless_init(int width, int height) if (!ctx->screen) goto error; - /* Updates run once per loop iteration (see poll) so damage coalesces */ + /* 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; diff --git a/src/twin_private.h b/src/twin_private.h index 6f1da4aa..d30acd39 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1903,10 +1903,12 @@ static inline void *_twin_widget_callback_data(const twin_widget_t *w) * use include/twin.h only */ -/* Work items run once per pass, lowest priority value first */ -#define TWIN_WORK_REDISPLAY 0 +/* 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_LAYOUT 2 +#define TWIN_WORK_REDISPLAY 2 twin_work_t *twin_set_work(twin_work_proc_t work_proc, int priority, diff --git a/src/window.c b/src/window.c index 40b98dae..6079ce1f 100644 --- a/src/window.c +++ b/src/window.c @@ -394,7 +394,9 @@ bool _twin_window_restore(twin_window_t *window) _twin_window_reset_clip(window); _twin_window_update_opaque(window); _twin_window_damage_all(window); - twin_window_draw(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; } From 5dd20705b277d22cdfdd918d9b336ac4f3b38e4d Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:22 +0800 Subject: [PATCH 42/48] Read each texel once when scaling images A transformed image read fetched (and, for RGB16, converted) four texels for every output pixel, though scaling up revisits the same four for several pixels in a row. They depend only on the integer source position, so they are fetched again only when it changes. --- src/draw-builtin.c | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/draw-builtin.c b/src/draw-builtin.c index 7c9dbb71..97c4bc8e 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -719,6 +719,8 @@ 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 + xform->off_y); @@ -731,10 +733,17 @@ static void twin_pixmap_read_xform_16(twin_xform_t *xform, twin_coord_t line) 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_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); + /* 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++) = @@ -756,6 +765,8 @@ 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 + xform->off_y); @@ -768,10 +779,17 @@ static void twin_pixmap_read_xform_32(twin_xform_t *xform, twin_coord_t line) 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_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); + /* 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++) = From 19962dcbba0db0b56acb1fc60ab790b7467ccf48 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:22 +0800 Subject: [PATCH 43/48] Keep photo thumbnails as RGB16 The Photos gallery keeps its six decoded thumbnails for as long as the grid or the viewer is open, 131 KB at 32 bits a pixel. Photos are opaque, so they are stored as RGB16: the peak heap over all phone apps drops from 719 KB to 664 KB, and the grid looks the same (the photos-grid golden is regenerated). --- apps/cellphone/photos.c | 19 ++++++++++++++++++- tests/golden/cellphone.txt | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/cellphone/photos.c b/apps/cellphone/photos.c index bb5a8f4c..af385b93 100644 --- a/apps/cellphone/photos.c +++ b/apps/cellphone/photos.c @@ -296,12 +296,29 @@ static struct { 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] = load_photo(i, "thumbs", THUMB, THUMB); + 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); diff --git a/tests/golden/cellphone.txt b/tests/golden/cellphone.txt index 528ee23f..923bad77 100644 --- a/tests/golden/cellphone.txt +++ b/tests/golden/cellphone.txt @@ -34,7 +34,7 @@ music-now-playing-dark bf354c9e music-list-playing 8503b11f music-list-scrolled 7a3b5121 music-list-reopened 9085b5b5 -photos-grid e6768493 +photos-grid 1f4043e5 photos-viewer bf0917c3 photos-viewer-first 76b2f0b1 photos-viewer-bare d6a2ef66 From fe0d825f9355d4f02a35c97f22bec2672c42e894 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 02:30:22 +0800 Subject: [PATCH 44/48] Speed up the Multi demo and mark opaque windows The blur window's box filter summed the whole window for every pixel and divided four times; it now keeps running sums and, for windows up to 16 pixels wide, multiplies by a reciprocal that is exact for every sum (checked exhaustively), with identical output. The Circletext, Smiley, ASCII and Flower windows fill white before anything that can fail, so every pixel is opaque, and now say so, which lets the compositor skip what lies under them. --- apps/multi.c | 67 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/apps/multi.c b/apps/multi.c index f03745da..4aed1fc4 100644 --- a/apps/multi.c +++ b/apps/multi.c @@ -202,12 +202,16 @@ static bool start_window(twin_screen_t *screen, const char *name, twin_draw_func_t draw, void *data, - twin_destroy_func_t destroy) + twin_destroy_func_t destroy, + bool opaque) { twin_window_t *window = twin_window_create( screen, TWIN_ARGB32, TwinWindowApplication, x, y, w, h); 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); @@ -219,17 +223,20 @@ static bool start_window(twin_screen_t *screen, return true; } -/* Paint @path in black over white */ -static void paint_text(twin_pixmap_t *pixmap, twin_path_t *path, int w, int h) +/* 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); - twin_paint_path(pixmap, 0xff000000, path); + 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; @@ -246,7 +253,7 @@ static void circletext_draw(twin_window_t *window) twin_path_utf8(path, "Hello, world!"); twin_path_restore(path, &state); } - paint_text(twin_window_pixmap(window), path, w, h); + twin_paint_path(pixmap, 0xff000000, path); twin_path_destroy(path); } @@ -254,10 +261,7 @@ static void smiley_draw(twin_window_t *window) { int w, h; client_size(window, &w, &h); - twin_pixmap_t *pixmap = twin_window_pixmap(window); - - /* Clear background to white */ - twin_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, 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); @@ -301,6 +305,7 @@ static void ascii_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; @@ -318,7 +323,7 @@ static void ascii_draw(twin_window_t *window) twin_path_utf8(path, "`abcdefghijklmnopqrstuvwxyz{|}~"); fy += D(s + 2); } - paint_text(twin_window_pixmap(window), path, w, h); + twin_paint_path(pixmap, 0xff000000, path); twin_path_destroy(path); } @@ -353,12 +358,11 @@ static void flower_draw(twin_window_t *window) { int w, h; client_size(window, &w, &h); - twin_pixmap_t *pixmap = twin_window_pixmap(window); + 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_fill(pixmap, 0xffffffff, TWIN_SOURCE, 0, 0, w, h); if (stroke && path) { twin_path_translate(path, cx, cy); twin_path_scale(path, twin_fixed_mul(scale, D(10)), @@ -393,18 +397,30 @@ static void box_blur_line(twin_argb32_t *p, { 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++) { - uint32_t sum[4] = {0, 0, 0, 0}, count = 0; - for (int k = i - r; k <= i + r; k++) { - if (k < 0 || k >= n) - continue; + for (; hi < n && hi <= i + r; hi++) for (int c = 0; c < 4; c++) - sum[c] += (tmp[k] >> (c * 8)) & 0xff; - count++; - } + 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) (sum[c] / count) << (c * 8); + v |= (twin_argb32_t) (fast ? (sum[c] * recip[count]) >> 16 + : sum[c] / count) + << (c * 8); p[i * step] = v; } } @@ -474,8 +490,9 @@ static bool apps_blur(twin_screen_t *screen, int x, int y, int w, int h) #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)) + blur_destroy, false)) return true; twin_pixmap_destroy(raw_background); return false; @@ -491,14 +508,14 @@ bool apps_multi_start(twin_screen_t *screen, (void) name; /* Started if any of its windows opened */ bool any = start_window(screen, x, y, w, h, "circletext", circletext_draw, - NULL, NULL); + 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); + smiley_draw, NULL, NULL, true); any |= start_window(screen, x += 20, y += 20, w, h, "ASCII", ascii_draw, - NULL, NULL); + NULL, NULL, true); any |= start_window(screen, x += 20, y += 20, w, h, "Flower", flower_draw, - NULL, NULL); + NULL, NULL, true); any |= apps_blur(screen, x += 20, y += 20, w / 2, h / 2); return any; } From ddb66023057a8ed45c13df41f43ed8c935289d29 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 06:15:56 +0800 Subject: [PATCH 45/48] Merge occlusion spans in place Adding an interval to a row's covered set rebuilt the set in a temporary array and copied it back through memcpy on every opaque layer of every row. Merge the touching intervals in place instead. The result is the same set, including the overflow case that keeps the old set; a randomized comparison against the old routine over two million sequences found no difference. --- src/screen.c | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/screen.c b/src/screen.c index 1620a8c7..f6477c07 100644 --- a/src/screen.c +++ b/src/screen.c @@ -229,30 +229,27 @@ static void _span_set_add(twin_span_set_t *set, twin_coord_t l, twin_coord_t r) { if (l >= r) return; - /* Merge with every interval that touches [l, r). */ - int i = 0, o = 0; - twin_span_t out[TWIN_SPAN_SET_MAX + 1]; - bool placed = false; - for (; i < set->n; i++) { - twin_span_t s = set->s[i]; - if (s.r < l) { - out[o++] = s; - } else if (r < s.l) { - if (!placed) { - out[o++] = (twin_span_t) {l, r}; - placed = true; - } - out[o++] = s; - } else { - l = min(l, s.l); - r = max(r, s.r); - } + /* 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); } - if (!placed) - out[o++] = (twin_span_t) {l, r}; + int o = n - (j - i) + 1; if (o > TWIN_SPAN_SET_MAX) return; /* keep the old (smaller) set: conservative */ - memcpy(set->s, out, sizeof(out[0]) * o); + 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; } From 0ee82e8a2f4b52da89c64ef9b6cb1cb819abd48d Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 06:38:46 +0800 Subject: [PATCH 46/48] Share one premultiplied over helper The built-in renderer and the screen compositor each kept a private copy of the same over operator. Keep one in twin_private.h, so the widget code can use it too. Output is bit-identical. --- src/draw-builtin.c | 20 ++------------------ src/screen-ops.c | 25 +++---------------------- src/twin_private.h | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/draw-builtin.c b/src/draw-builtin.c index 97c4bc8e..1d285b62 100644 --- a/src/draw-builtin.c +++ b/src/draw-builtin.c @@ -56,22 +56,6 @@ static inline twin_argb32_t in(twin_argb32_t src, twin_a8_t msk) _twin_mul_lanes((src >> 8) & TWIN_LANES, msk) << 8; } -static inline twin_argb32_t 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; -} - static inline twin_argb32_t in_over(twin_argb32_t dst, twin_argb32_t src, twin_a8_t msk) @@ -80,7 +64,7 @@ static inline twin_argb32_t in_over(twin_argb32_t dst, return dst; if (msk != 0xff) src = in(src, msk); - return over(dst, src); + return _twin_over(dst, src); } static inline twin_argb32_t rgb16_to_argb32(twin_rgb16_t v) @@ -193,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); \ } \ } diff --git a/src/screen-ops.c b/src/screen-ops.c index 48db9e36..036e9549 100644 --- a/src/screen-ops.c +++ b/src/screen-ops.c @@ -10,25 +10,6 @@ #include "twin_private.h" -/* - * Porter-Duff "over" operator for ARGB32 pixels - * dst = src + dst * (1 - alpha_src) - */ -static inline twin_argb32_t over(twin_argb32_t dst, twin_argb32_t src) -{ - uint32_t a = 0xff - (src >> 24); - - if (a == 0) - return src; - if (!src) - return dst; - return _twin_sat_lanes(_twin_mul_lanes(dst & TWIN_LANES, a) + - (src & TWIN_LANES)) | - _twin_sat_lanes(_twin_mul_lanes((dst >> 8) & TWIN_LANES, a) + - ((src >> 8) & TWIN_LANES)) - << 8; -} - /* * The built-in renderer generates these three span operations along with the * rest of its compositing table; other renderers (Pixman) get them here. @@ -60,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; } } @@ -94,7 +75,7 @@ void _twin_screen_blend_argb32(twin_argb32_t *dst, int width) { while (width--) { - *dst = over(*dst, in_alpha(*src++, alpha)); + *dst = _twin_over(*dst, in_alpha(*src++, alpha)); dst++; } } @@ -107,7 +88,7 @@ void _twin_screen_blend_rgb16(twin_argb32_t *dst, while (width--) { twin_rgb16_t v = *src++; twin_argb32_t s = twin_rgb16_to_argb32(v); - *dst = over(*dst, in_alpha(s, alpha)); + *dst = _twin_over(*dst, in_alpha(s, alpha)); dst++; } } diff --git a/src/twin_private.h b/src/twin_private.h index d30acd39..5aeff816 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -754,6 +754,23 @@ static inline uint32_t _twin_sat_lanes(uint32_t x) 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)) From 220ebb8dcbd6337833ba1c521e8bdd90c8c1d0f1 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 06:38:46 +0800 Subject: [PATCH 47/48] Draw the Vectors image over its background The image demo copied each vector drawing into its window with SOURCE, so the drawing's transparent pixels replaced the widget's background and the desktop showed through around the tiger. Composite it with OVER onto the background the widget already filled. --- apps/image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/image.c b/apps/image.c index d05c8f1c..4693535c 100644 --- a/apps/image.c +++ b/apps/image.c @@ -56,7 +56,7 @@ 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, twin_pixmap_width(img->current_pix), + TWIN_OVER, twin_pixmap_width(img->current_pix), twin_pixmap_height(img->current_pix)); } From 122acdcc8f883741f70e22faa1e1d0b16d3a9e60 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Mon, 19 Oct 2026 06:38:46 +0800 Subject: [PATCH 48/48] Paint toplevel backgrounds and declare them opaque A toplevel's box had a transparent background and child widgets cleared their rectangle to their own, mostly transparent, color. The demo windows (Calculator, Clock, Spline, Vectors) were therefore see-through: the wallpaper and even the window's own shadow showed in their gaps, and the compositor had to blend everything underneath. In the phone, the app sliding away showed through a demo sliding in. A toplevel now starts with an opaque white background, and its window is opaque whenever the root's background is opaque and rectangular. A widget fills its rectangle with its background composited over its ancestors' backgrounds, so a repaint erases the old content without punching a hole, and a translucent shape no longer darkens with each repaint. A box whose children all repaint whole fills itself first, so space no child covers shows its background, and changing a box's background or shape repaints its descendants. The new twin_box_widget gives applications a box's widget; the Calculator uses it to keep the black panel its translucent white display and keys were drawn for. The phone and desktop demo suites run 11% fewer instructions. --- apps/calc.c | 4 ++ include/twin.h | 9 +++- src/box.c | 35 ++++++++++--- src/toplevel.c | 3 ++ src/twin_private.h | 2 + src/widget.c | 52 +++++++++++++++++-- tests/test-core.c | 127 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 220 insertions(+), 12 deletions(-) diff --git a/apps/calc.c b/apps/calc.c index cbbab09f..4cab58ef 100644 --- a/apps/calc.c +++ b/apps/calc.c @@ -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 @@ -223,6 +226,7 @@ bool apps_calc_start(twin_screen_t *screen, return false; } twin_box_t *top = twin_toplevel_box(calc->toplevel); + twin_widget_set(twin_box_widget(top), APPS_CALC_BG); calc->display = twin_label_create(top, "0", APPS_CALC_VALUE_FG, APPS_CALC_VALUE_SIZE, APPS_CALC_VALUE_STYLE); diff --git a/include/twin.h b/include/twin.h index 3ff4ee72..0f12ce3f 100644 --- a/include/twin.h +++ b/include/twin.h @@ -592,7 +592,9 @@ void twin_label_set(twin_label_t *label, const char *value); void twin_label_set_align(twin_label_t *label, twin_align_t align); -/** The widget a label or button is built on, for the twin_widget_* calls */ +/** 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); @@ -801,6 +803,11 @@ 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_coord_t x, twin_coord_t y, diff --git a/src/box.c b/src/box.c index cd4ffa1b..fd90ed22 100644 --- a/src/box.c +++ b/src/box.c @@ -156,6 +156,21 @@ void _twin_box_mark_paint(twin_box_t *box) } } +/* 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) @@ -201,7 +216,14 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, _twin_widget_callback_data(child)); } 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) { @@ -210,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; @@ -223,6 +240,7 @@ twin_dispatch_result_t _twin_box_dispatch(twin_widget_t *widget, twin_pixmap_set_origin(pixmap, ox, oy); } break; + } default: break; } @@ -238,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/toplevel.c b/src/toplevel.c index 4ebe8a62..647f5c8b 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -117,6 +117,9 @@ twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen, 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; } diff --git a/src/twin_private.h b/src/twin_private.h index 5aeff816..463b06be 100644 --- a/src/twin_private.h +++ b/src/twin_private.h @@ -1048,6 +1048,8 @@ void _twin_box_init(twin_box_t *box, /* 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, diff --git a/src/widget.c b/src/widget.c index 716e06ef..b4146f1b 100644 --- a/src/widget.c +++ b/src/widget.c @@ -11,17 +11,34 @@ #include "twin_private.h" -/* Fill the widget with its background, in its shape */ -static void _twin_widget_paint(twin_widget_t *widget) +/* What the ancestors' rectangular backgrounds leave under @widget */ +static twin_argb32_t _twin_widget_under(const twin_widget_t *widget) +{ + 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); +} + +/* 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 (widget->shape == TwinShapeRectangle) { - twin_fill(pixmap, widget->background, TWIN_SOURCE, 0, 0, w, h); + 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; @@ -174,16 +191,41 @@ void _twin_widget_bevel(twin_widget_t *widget, twin_fixed_t b, bool down) } } +/* 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) +{ + 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); +} + +/* The background or shape of @widget changed */ +static void _twin_widget_restyle(twin_widget_t *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); } void twin_widget_set_shape(twin_widget_t *widget, twin_shape_t shape) { widget->shape = shape; - _twin_widget_queue_paint(widget); + _twin_widget_restyle(widget); } void twin_widget_match_size(twin_widget_t *widget, twin_widget_t *source) diff --git a/tests/test-core.c b/tests/test-core.c index 793ec037..5b615330 100644 --- a/tests/test-core.c +++ b/tests/test-core.c @@ -1208,6 +1208,131 @@ static void test_accessors(void) 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 @@ -1335,6 +1460,8 @@ int main(void) test_accessors(); #if defined(CONFIG_WIDGETS) test_widget_teardown(); + test_toplevel_background(); + test_box_background(); #endif #if defined(CONFIG_LOADER_GIF) test_gif_malformed();