🐛 Freeze viewport gestures during WASM page transition (#11301)

Pan/zoom via render_from_cache while the tile atlas is still empty
left a blank workspace under the page-transition blur. Ignore
set-view-box / view-interaction-start until tiles-complete, block
pointer events on the viewport SVG, and flush any deferred local
viewport sync when the overlay ends.
This commit is contained in:
Alejandro Alonso 2026-08-21 14:19:50 +02:00 committed by GitHub
parent 6d4a6f6a9a
commit 8aefa2ddfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 21 deletions

View File

@ -709,7 +709,8 @@
:global/cursor-resize-ew-0 (= @guide-hover-axis* :x) :global/cursor-resize-ew-0 (= @guide-hover-axis* :x)
:global/cursor-resize-ns-0 (= @guide-hover-axis* :y) :global/cursor-resize-ns-0 (= @guide-hover-axis* :y)
:viewport-controls true)) :viewport-controls true))
:style {:touch-action "none"} :style {:touch-action "none"
:pointer-events (if page-transition? "none" "auto")}
:fill "none" :fill "none"
:on-click on-click :on-click on-click
:on-context-menu on-context-menu :on-context-menu on-context-menu

View File

@ -83,6 +83,7 @@
;; `penpot:wasm:tiles-complete`. ;; `penpot:wasm:tiles-complete`.
;; ;;
;; - `page-transition?`: true while the overlay should be considered active. ;; - `page-transition?`: true while the overlay should be considered active.
;; Pan/zoom into WASM is frozen until `tiles-complete` (atlas still empty).
;; - `transition-image*`: image shown by the UI overlay (usually an `ImageBitmap` ;; - `transition-image*`: image shown by the UI overlay (usually an `ImageBitmap`
;; snapshot of the WebGL canvas; on initial load it may be a tiny SVG data-url ;; snapshot of the WebGL canvas; on initial load it may be a tiny SVG data-url
;; string derived from the page background color). ;; string derived from the page background color).
@ -92,6 +93,8 @@
;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely. ;; `penpot:wasm:tiles-complete`, so we can remove/replace it safely.
(defonce page-transition? (atom false)) (defonce page-transition? (atom false))
(defonce context-loss-overlay? (atom false)) (defonce context-loss-overlay? (atom false))
;; Skipped set-view-box during transition; flushed when the overlay ends.
(defonce ^:private viewport-dirty-during-transition? (atom false))
;; When true (initial load) the overlay clips out the ruler strips so the live ;; When true (initial load) the overlay clips out the ruler strips so the live
;; rulers show through. False (page switch / context loss) keeps the snapshot's ;; rulers show through. False (page switch / context loss) keeps the snapshot's
;; baked-in rulers full-bleed to avoid a blank-strip flicker on canvas remount. ;; baked-in rulers full-bleed to avoid a blank-strip flicker on canvas remount.
@ -114,6 +117,8 @@
[] []
(wasm/ready?)) (wasm/ready?))
(declare sync-workspace-local-viewport!)
(defn set-transition-image-from-background! (defn set-transition-image-from-background!
"Sets `transition-image*` to a data URL representing a solid background color." "Sets `transition-image*` to a data URL representing a solid background color."
@ -128,6 +133,7 @@
(defn begin-page-transition! (defn begin-page-transition!
[] []
(reset! page-transition? true) (reset! page-transition? true)
(reset! viewport-dirty-during-transition? false)
(swap! transition-epoch* inc)) (swap! transition-epoch* inc))
(defn end-page-transition! (defn end-page-transition!
@ -136,7 +142,11 @@
(when-let [prev @transition-tiles-handler*] (when-let [prev @transition-tiles-handler*]
(.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev)) (.removeEventListener ^js ug/document "penpot:wasm:tiles-complete" prev))
(reset! transition-tiles-handler* nil) (reset! transition-tiles-handler* nil)
(reset! transition-image* nil)) (reset! transition-image* nil)
;; Keyboard/wheel may have moved workspace-local while WASM was frozen.
(when (and (initialized?) @viewport-dirty-during-transition?)
(reset! viewport-dirty-during-transition? false)
(sync-workspace-local-viewport! @st/state)))
(defn- set-transition-tiles-complete-handler! (defn- set-transition-tiles-complete-handler!
"Installs a tiles-complete handler bound to the current transition epoch. "Installs a tiles-complete handler bound to the current transition epoch.
@ -1383,7 +1393,9 @@
(defn view-interaction-start! (defn view-interaction-start!
[] []
(when (and (initialized?) (not @view-interaction-active?)) (when (and (initialized?)
(not @page-transition?)
(not @view-interaction-active?))
(h/call wasm/internal-module "_set_view_start") (h/call wasm/internal-module "_set_view_start")
(reset! view-interaction-active? true))) (reset! view-interaction-active? true)))
@ -1463,25 +1475,29 @@
(defn set-view-box (defn set-view-box
[zoom vbox] [zoom vbox]
(when (initialized?) ;; Frozen during page transition: tile atlas is empty/incomplete and
(perf/begin-measure "set-view-box") ;; render_from_cache would present a blank workspace.
(view-interaction-start!) (if @page-transition?
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) (reset! viewport-dirty-during-transition? true)
(perf/end-measure "set-view-box") (when (initialized?)
(perf/begin-measure "set-view-box")
(view-interaction-start!)
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(perf/end-measure "set-view-box")
(perf/begin-measure "render-from-cache") (perf/begin-measure "render-from-cache")
(h/call wasm/internal-module "_render_from_cache" 0) (h/call wasm/internal-module "_render_from_cache" 0)
;; Keep the text-editor caret/selection glued to the shapes while the view ;; Keep the text-editor caret/selection glued to the shapes while the view
;; changes. `_render_from_cache` re-composites shapes + UI at the new viewbox ;; changes. `_render_from_cache` re-composites shapes + UI at the new viewbox
;; but omits the editor overlay, so without this the selection would vanish for ;; but omits the editor overlay, so without this the selection would vanish for
;; the whole pan/zoom gesture and only flash back when the debounced full ;; the whole pan/zoom gesture and only flash back when the debounced full
;; render lands — the blink seen when zooming in/out over a selection at high ;; render lands — the blink seen when zooming in/out over a selection at high
;; zoom (gh-10709). `_text_editor_render_overlay` draws straight onto the ;; zoom (gh-10709). `_text_editor_render_overlay` draws straight onto the
;; freshly composited Target (no Backbuffer re-compose) and no-ops when no ;; freshly composited Target (no Backbuffer re-compose) and no-ops when no
;; editor is active. ;; editor is active.
(render-text-editor-overlay-if-active!) (render-text-editor-overlay-if-active!)
(render-finish) (render-finish)
(perf/end-measure "render-from-cache"))) (perf/end-measure "render-from-cache"))))
(defn sync-workspace-local-viewport! (defn sync-workspace-local-viewport!
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM." "Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."