Add mobile and desktop window managers with phone UI

Merged
JS
2h ago
+30061-5779

This turns window management into a policy layer and builds a phone UI on top of it. A build selects one window manager in Kconfig, and every screen runs it for its whole life, with no run-time switching or checks: desktop (the default: decorated overlapping windows with maximize, edge snapping, corner resize and focus cycling), mobile (an application stack over the work area between system bars, with push/pop/dismiss transitions, an edge-swipe back gesture and eviction of covered apps' pixels), or none (kiosk). The compositor stays policy-free. apps/cellphone ports the cellphone prototype onto the mobile policy: status and navigation bars, a lock screen, home and a dozen apps on a small immediate-mode view kit.

apps/apps.c registers every demo app. Which window managers an app runs under is settled at build time too: the demos run under all three, and the phone shell depends on WM_MOBILE and is compiled in only there. The demo launcher starts the phone shell when it is built and otherwise every app (only the first under none); the phone's home screen lists the other apps, whose windows Back and Home close like phone screens. include/twin.h now carries only the public interface; object layouts and the backend interface moved to src/twin_private.h and applications use accessors. New calls: twin_window_back() with TwinEventBack, twin_window_activate(), twin_screen_focus_next(), twin_screen_set_inset() for an on-screen keyboard, twin_toplevel_destroy() and twin_box_widget(). A toplevel now paints an opaque background (white by default) and its window is opaque unless the application gives its box a translucent background; widgets fill over their ancestors' backgrounds instead of clearing to their own, so the demos no longer show the wallpaper, their own shadow or, in the phone, the previous app through their gaps. docs/phone.md describes the design and the trade-offs.

Verification: make check runs the in-process headless suites with a virtual clock; the core and phone suites compare bit-exact golden frames. The desktop build runs core (300 checks), wm (73), lifetime (21) and demos (122); the mobile build runs wm (103), demos (112) and cellphone (911); the none build runs the none-only wm cases (28). test-demos runs every registered app under the built policy, closing it window by window, checking that revealed windows are repainted, that a start which runs out of memory leaves nothing behind, and that nothing leaks. All suites pass in 22 configurations (desktop, mobile and none builds, plus no coverage cache, no eviction, no memory stats, no widgets, RGB565 surfaces, a TLSF heap, Pixman, no shadows, a size-optimized build without a scratch arena, and the mobile policy without the phone, across window managers where they apply), under ASan/UBSan with -fno-sanitize-recover=undefined and under Valgrind for all three window managers. Every commit in the series builds. The rendering arithmetic that replaced 64-bit divides and float is checked against the old forms (exhaustively where feasible, e.g. 1.7e7 twin_fixed_div pairs), and the rv32im objects link no soft-float or 64-bit divide helpers in the per-pixel paths. The fbdev and DRM backends share one core now; they were built and checked for clean failure on a missing device, but no framebuffer or DRM device was available to test on hardware.

Performance: the hotspots were found with uftrace on the headless suites and checked with perf. The window a transition dims is blended straight onto the background through a 256-entry product table; large paths are rasterized in bands from one edge list instead of through a full-size heap mask; arcs are flattened to the 1/16 px the rasterizer resolves; the active edge table is an array; the shadow fringe keeps its alphas between rows; scaled images read each texel once; paint work runs before redisplay and windows revealed while a stack of apps closes are painted only if they stay visible; toplevel windows are opaque, so nothing under them is composited. On an x86-64 host, minimum of five runs of perf stat, before this round (7c5118e) and after: the phone suite (test-cellphone, mobile build) runs 14.32G to 7.57G instructions (-47%) and 5.26G to 2.67G cycles (-49%); the desktop demo suite (test-demos) runs 1.98G to 1.25G instructions (-37%) and 0.89G to 0.59G cycles (-33%). The phone's peak heap over all apps falls from 725,372 to 664,000 bytes, mostly from the transient masks banding removed and from photo thumbnails kept as RGB565. The rv32im library text grows about 4 KB. Output stays bit-identical except arcs, whose antialiased edges move by a few pixels per screen (the goldens and screenshots below are regenerated), and the photo grid, whose thumbnails are now RGB565.

Out-of-memory handling is checked where the apps own it (window and widget creation); the core text and path code still dereferences NULL when an allocation fails while painting, which predates this change. Not included: a recents switcher and a notification layer for the phone shell. The WM hooks they need (twin_window_activate, twin_screen_focus_next, the Back event) are in place, but they would change the ported prototype's navigation model. The WASM and VNC backends were not built locally.

Screenshots

Cellphone screens at their native 240x320, unscaled, rendered by make check (MADO_SCREENSHOTS) from this branch in the mobile build. They use the default Sage theme: warm white pages, sage green grouping and deep olive bars, with header titles centered, launcher tiles as touch targets, single-color settings symbols and the current navigation tab filled in the accent. docs/phone.md describes the design language.

Lock, home and the second home page with the shared demo apps, contacts:

lock home home-demos contacts-list

Contact card, dialer, an active call, messages:

contacts-detail dialer-number dialer-incall sms-list

Conversation, now playing, photos, call log:

sms-chat music-now-playing photos-grid calllog

Settings, theme choice, a game, home in the Dark theme:

settings settings-display games-tetris settings-home-dark

Closes #39
Closes #160


Summary by cubic

Turns window management into a build-time policy and ports the cellphone UI to the demo. A build picks one window manager in Kconfig — desktop (decorated overlapping windows), mobile (an app stack over the work area between system bars, with push/pop/dismiss transitions and edge-swipe back), or none (kiosk) — and a screen enters the built policy on creation, calling it through compiled hooks with no run-time registry. Fields only one policy uses compile out of the others, and a destroyed screen tears its windows down without the policy; static archives are rebuilt from scratch so a member from a previous configuration never lingers. The compositor gains multi-rectangle damage, occlusion culling, a coverage cache, synthesized drop shadows and retained-pixel scrolling, with embedded Montserrat and Font Awesome fonts. Rendering avoids per-pixel 64-bit divides and soft-float so the rv32im build links no floating-point support and the TinyVG loader shrinks 3.3 KB, with bit-identical output; the Pixman renderer now passes the whole source transform, and the built-in renderer and compositor share one premultiplied over helper. Resident and static memory shrink as well: masks too big for the scratch arena come from the heap, the arena size is a Kconfig option, an RGB16 mask composites as none so 24 compositing functions are gone, the closure tracker grows past its inline table, and OPTIMIZE_SIZE builds with -Os.

Cellphone app and public API

  • apps/cellphone ports the phone prototype in the new default Sage theme — three named surfaces, deep olive bars, warm white pages — with status and navigation bars, a lock screen, home and a dozen apps on a small immediate-mode UI kit. apps/apps.c registers every demo, the phone shell compiles in only under WM_MOBILE, and launchers start apps in registry order, multi-window demo first, with phone Back, Home and nav bar acting on the topmost application window whether or not it is a phone view.
  • Toplevels start with an opaque white background, so their windows are opaque and the compositor skips everything under them; widgets fill composited over their ancestors' backgrounds, so demo windows no longer show the wallpaper or their own shadow through their gaps, and a repaint erases without punching a hole. The image demo composites its drawing over its background instead of replacing it.
  • Demo apps tear down and report failure when a widget or callback cannot be created; phone shutdown closes every app window, a shown toplevel paints its widgets once instead of twice, and bars are re-hidden on every relayout because a bar's pixmap can still be shown directly through the pixmap API.
  • include/twin.h now holds only the public interface; object layouts and the backend interface moved to src/twin_private.h and applications use accessors. Calls lose parameters every caller passed the same way: twin_toplevel_create() always makes an ARGB32 app window, twin_pixmap_from_file() always decodes to ARGB32, twin_label_set() takes text only, and twin_run() runs the dispatch loop itself and hands its callback the screen. twin_create() installs the arrow cursor, and twin_tvg_to_pixmap() replaces the two TinyVG entry points. New calls add phone navigation plus twin_window_activate(), twin_screen_focus_next(), twin_screen_set_inset() and twin_toplevel_destroy(). Negative text origins no longer trip UBSan, tweens always deliver their final frame, TrueType metrics report real bearings, and malformed GIFs fail cleanly instead of looping or writing past their LZW stream.

Verification

  • make check runs four headless in-process suites (core 614, wm 252, lifetime 24, cellphone 839 checks) with a virtual clock; core and phone compare bit-exact golden frames, missing or malformed goldens fail instead of being skipped, and the wm suite checks that covered windows cost no pixels only when eviction is built. tox-core also covers Pixman-transformed RGB16 reads and the TinyVG hairline pen.
  • fbdev and DRM read evdev on the caller's thread via twin_linux_input_poll() instead of an unsynchronized second thread, and backends sleep until input or the next timeout instead of spinning; VNC no longer leaks or reads uninitialized damage state, the headless backend records its owning PID, headless-ctl validates UTF-8 before typing, and the DRM backend forgets its framebuffer mapping on unmap. fbdev and DRM now share one core in backend/linux_fb.h.
  • All pass in 16 configurations and under ASan/UBSan (including the WM_NONE build) and Valgrind, plus CI lanes without shadows and with OPTIMIZE_SIZE and no scratch arena; test-demos runs every app under the built policy, also with the first allocation failing, and the phone suite re-tests demos with the work area shrunk by the keyboard inset.