1) Tap/touch during a fling: the ListView's Flickable already stops the
fling when a press lands (its press handler resets the fling
animation — slint core items/flickable.rs, remove_binding =
set_animated_value(current, none)); the bug was that the press is
then forwarded to the cell (~100 ms later, FORWARD_DELAY) and the
release opened the viewer. The cell TouchArea now reports its
pointer-event down (1.18 has no pressed callback) via
cell-pressed → on_cell_pressing, which marks the tap as a
fling-stopper when the grid moved within the last 150 ms (the tick
tracks raw list-y changes in grid_motion); on_open_image then
swallows the tap.
2) Zooming past max resolution moved the image: zoom_at applied the
raw factor to tx/ty BEFORE clamping s — at the cap s stayed put
but the translation still shifted about the (moving) pinch
center, so the image followed the fingers like a two-finger drag.
zoom_at now clamps s first (factor derived from the clamped s:
at the cap factor = 1, a true no-op), and Input::pinch sticks at
both caps (the handler reports a CUMULATIVE scale from the pinch
start, so past the cap every later update would otherwise drift).
The zoom cap is now NATIVE resolution: max(fit, 1.0) — 1 device
pixel per image pixel (small images keep fit); zooming further
only pixelates, so it is not allowed (was 20× fit).
Tests: zoom_at_past_cap_is_noop, pinch_sticks_at_caps (new),
clamp_zoom_bounds (native cap + small-image fit), updated
pinch_handler_sequence (4× fit now clamps at native). 36/36.
Emulator: fling+tap → 0 viewer opens, grid settled; normal tap
still opens (cached path).
open_viewer never shows 'Loading…' anymore:
1. full-res LRU cache hit (8 entries / 64 MP of RGBA bytes) -> the
image is shown in the same frame as the tap (decode already done
by idle prefetch or a previous view); the ~3 ms GPU upload is the
grid's proven hitch-free path (slint::Image is !Send, so the
cache holds bytes and uploads on tap);
2. else the tapped cell's THUMBNAIL (already a GPU texture, same
aspect) is shown instantly and the full decode (urgent: bypasses
the idle budget) swaps in silently on arrival - same aspect, so
s is rescaled by old/new (Input::rebase_content), tx/ty (viewer
px) stay put: identical bounds, one content change, any
pan/zoom/pinch the user did on the thumb is preserved;
3. only if the cell has no painted thumb either is there a black
frame (ViewerUi.waiting names the path the viewer waits on; a
failed decode prints an error only for that path).
Tick step 4c (prefetch_full): when the viewport is stable, the
viewer is closed and the thumb pipeline is caught up (inflight
empty after FILL - during fast scrolling it stays non-empty, so
prefetch only starts once scrolling is done), decode + cache one
center-out visible image per tick, max 2 in flight. In-flight
decodes are NOT cancelled by scrolling: results still land in the
cache (that IS the value). All caches clear on set_list / world
reset.
Verified on the emulator: cache-hit tap '(full 1200x1600 cached)'
instant; uncached tap '(thumb 159x400 up, full in flight)' ->
'(swapped in)' 137 ms later; corrupt file -> error text, no splash.
New unit test: rebase_content_preserves_viewer_mapping (34 pass).
- src/viewer.rs: full-size decode (turbojpeg DCT shrink, 2x-screen cap
bounded by GPU max texture, EXIF orientation applied) + transform
model (fit/clamp/zoom_at) + single-finger pan & double-tap state
machine. 18 unit tests.
- Pinch: Slint 1.18's ScaleRotateGestureHandler. Key finding: the CORE
(TouchState in internal/core/input.rs), not the backend, synthesizes
PinchGesture from raw two-finger touches on every backend including
Android — the second finger never reaches a TouchArea, so manual
two-finger math is dead code (the earlier draft was removed).
- main.slint: Viewer overlay (image + status + close button + full-
surface TouchArea + ScaleRotateGestureHandler). The close button must
be declared AFTER the full-surface TouchArea: 1.18 TouchArea grabs
every press (GrabMouse) and aborts the front-to-back sibling
traversal.
- lib.rs: open/close/pinch/pointer wiring; Android back key (via the
vendor CloseRequested patch) closes the viewer first.
- vendor: androidwindowadapter back-key -> CloseRequested patch
(Slint 1.18 has no back-key close handling).
- gputex.rs: max_texture_size() for the decode cap.
- HANDOFF: M4 done; new gotchas 6.14-16; emulator two-finger sendevent
(MT-B) injection recipe.
Verified on emulator (pad_avd): real two-finger pinch zoom, double-tap
zoom/reset, pan, button + back close, grid scroll unaffected. Phone
(Pixel 9 Pro, release aarch64): grid, viewer open, 4080x3072 decode,
double-tap, back. Pinch not injectable on the user build (SELinux
blocks /dev/input writes) — covered by unit tests + emulator.