Add touch gestures to the image lightbox - #7097
Open
void-function865 wants to merge 1 commit into
Open
Conversation
Add mobile touch gestures to the lightbox (stashapp#2538), driven by a single-finger axis-locked gesture state machine in LightboxImage: - swipe left/right: previous/next image (only when not horizontally pannable; a zoomed-in horizontal drag still pans) - swipe up: delete the current image via the existing confirmation dialog (never bypassed; only when not vertically pannable) - swipe down: close the lightbox - double-tap: zoom toward the tapped point, toggling between fit and 1:1 native (capped) - pinch-to-zoom is reworked to anchor on the pinch focal point and is computed absolutely from a snapshot taken at gesture start (stable, no per-frame drift) The axis (horizontal/vertical/pan) is locked once movement passes a threshold, decided from the image-vs-box geometry so panning only happens when the image actually overflows; panning (touch and mouse) clamps to the image edges. A single tap is a no-op on touch; desktop mouse/keyboard behaviour is unchanged (a ghost-click guard stops the synthetic click iOS fires after a tap from navigating). Supporting changes: - cap the maximum zoom per image and per device, since a very large image at high zoom can exhaust GPU memory and crash the tab on phones (small images reach the full ceiling everywhere) - show a busy spinner, on its own compositor layer, for the first (expensive) zoom-in of a large image - track the container size with a ResizeObserver so fit/centering follow window resizes and device rotation, preserving the focal point and zoom when zoomed in Document the gestures under the Lightbox section of the keyboard shortcuts manual.
void-function865
added a commit
to void-function865/stash
that referenced
this pull request
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This adds mobile/touch gesture support to the image lightbox. On a phone or tablet you can now swipe between images, swipe to delete or close, and double-tap or pinch to zoom — previously the lightbox was only usable on touch through the on-screen buttons.
While zoomed in, a drag pans the image (clamped to its edges) instead of navigating, deleting or closing.
The lightbox also now responds to device rotation and window resizing, re-fitting the image to the new viewport (and keeping a zoomed-in image centered on the same point) instead of staying laid out for the previous orientation.
Related Issue
Closes #2538
Testing
Desktop behaviour is unchanged: all the gesture handling is touch-gated, so mouse/keyboard navigation, scroll-zoom, the nav buttons and the delete/O/rating controls behave exactly as before.
Tested on real devices (iPhone and iPad) as well as across Playwright touch-emulation profiles — Pixel 5 (Chromium), iPhone and iPad (WebKit):
Screenshots
No new UI.
Checklist
AI Usage Disclosure
I used Claude Code as a coding assistant for this PR. It helped explore the hand-rolled lightbox, draft and implement the approach (the axis-locked single-finger gesture state machine, the swipe/double-tap/pinch handling, the image- and device-aware zoom cap, the compositor-layer busy spinner, and the
ResizeObserver-driven fit/centering/pan bounds), and author the Playwright end-to-end tests used to verify it. I directed the design decisions, reviewed all changes, and validated the behavior on real devices (iPhone and iPad) as well as through browser touch emulation and the automated tests before submitting.Additional Context
Key design choices:
DeleteImagesDialog; it only ever opens the dialog, exactly like the delete button, and is disabled when the current image has no id.closeOnVerticalDrag). Vertical gestures only fire at fit zoom; once zoomed in, a vertical drag pans instead.getBoundingClientRect) drifted and could wedge the view; the absolute form (pan' = (focal − centre)(1 − r) + r·startPan) stays stable for the whole pinch.clickshortly after a tap; a short timestamp guard suppresses it so a tap doesn't also trigger the desktop half-screen click-to-navigate. Real mouse clicks on desktop are unaffected — taps zoom, clicks still advance.navigator.deviceMemorywhere available, else a phone-vs-tablet screen-size proxy on Safari); every other image keeps the full zoom ceiling on every device. The cap feeds the pinch and double-tap clamps as well as the existing scroll/keyboard zoom.ResizeObserver. Fit-zoom, centering, the axis decision and the pan bounds all depend on the viewport box, so they now follow window resizes and device rotation (previously measured once at mount, which left a zoomed image mis-centered after a rotate). On rotate at fit it re-fits; while zoomed it preserves the current scale and focal point and re-clamps. This also removed acreateRef()-per-render.Related PR: #6197 also targets #2538, but is narrower. The issue itself asks for swipe left/right navigation and double-tap-to-zoom; #6197 implements the swipe navigation and makes panning stop at the image edge (reworking the pan-position model so 0 means centered, with a non-zero position being an offset), while this PR implements both of the issue's asks and adds further gestures beyond it — pinch zoom, and swipe up/down to delete/close — plus the supporting zoom cap and rotation handling. The two touch the same
Lightbox.tsx/LightboxImage.tsxand overlap on swipe navigation and the pan-edge behaviour (and take different approaches to the pan-position model).Known device limitation: rotating a very large image (tested with a ~140 MP photo) on a memory-constrained iPhone can briefly stutter or freeze the tab. This is an iOS Safari decoded-image/canvas memory ceiling, not this code — it reproduces with the resize handling disabled, because the rotate itself forces the browser to re-raster the full-resolution bitmap. The real fix is serving a downscaled image to the lightbox, a separate Stash-wide change that's out of scope here. Normal-sized images are unaffected.