Merge pull request #9523 from penpot/superalex-test-firefox

🐛 Fix WASM viewport interaction lifecycle for pan/zoom
This commit is contained in:
Alejandro Alonso 2026-05-12 15:34:25 +02:00 committed by GitHub
commit 63ff5c87c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 158 additions and 41 deletions

View File

@ -1578,6 +1578,7 @@
(dm/export dwv/initialize-viewport)
(dm/export dwv/update-viewport-position)
(dm/export dwv/update-viewport-size)
(dm/export dwv/sync-wasm-workspace-viewport)
(dm/export dwv/start-panning)
(dm/export dwv/finish-panning)

View File

@ -16,13 +16,19 @@
[app.common.math :as mth]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.viewport-wasm :as dwvw]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn- render-context-lost?
[state]
(true? (get-in state [:render-state :lost])))
(defn sync-wasm-workspace-viewport
"Effect-only: pushes the current workspace zoom/view box to WASM after other
events (e.g. `update-viewport-size`) have updated the store."
[]
(ptk/reify ::sync-wasm-workspace-viewport
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(defn initialize-viewport
[{:keys [width height] :as size}]
@ -86,7 +92,11 @@
(update [_ state]
(update state :workspace-local
(fn [local]
(setup state local)))))))
(setup state local))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state)))))
(defn calculate-centered-viewbox
"Updates the viewbox coordinates for a given center position"
@ -105,9 +115,13 @@
(ptk/reify ::update-viewport-position-center
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(update state :workspace-local calculate-centered-viewbox position)))))
(update state :workspace-local calculate-centered-viewbox position)))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(defn update-viewport-position
[{:keys [x y] :or {x identity y identity}}]
@ -124,13 +138,17 @@
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(update-in state [:workspace-local :vbox]
(fn [vbox]
(-> vbox
(update :x x)
(update :y y))))))))
(update :y y))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(defn update-viewport-size
[resize-type {:keys [width height] :as size}]
@ -174,16 +192,27 @@
(assoc-in [:vbox :width] vbox-width')
(assoc-in [:vbox :height] vbox-height')))))))))
(defn- activate-panning []
(ptk/reify ::activate-panning
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :panning] true)))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-view-interaction-start! state))))
(defn start-panning []
(ptk/reify ::start-panning
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
zoom (get-in state [:workspace-local :zoom])]
(when (and (not (render-context-lost? state))
(when (and (not (dwvw/render-context-lost? state))
(not (get-in state [:workspace-local :panning])))
(rx/concat
(rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
(rx/of (activate-panning))
(->> stream
(rx/filter mse/pointer-event?)
(rx/filter #(some? (mse/get-pointer-movement %)))
@ -200,4 +229,8 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :panning)))))
(update :workspace-local dissoc :panning)))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-view-interaction-end! state))))

View File

@ -0,0 +1,30 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.viewport-wasm
(:require
[app.main.features :as features]
[app.render-wasm.api :as wasm.api]))
(defn render-context-lost?
[state]
(true? (get-in state [:render-state :lost])))
(defn maybe-sync-workspace-local-viewport!
"When `render-wasm/v1` is active, pushes workspace zoom and vbox into WASM."
[state]
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
(wasm.api/sync-workspace-local-viewport! state)))
(defn maybe-view-interaction-start!
[state]
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
(wasm.api/view-interaction-start!)))
(defn maybe-view-interaction-end!
[state]
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
(wasm.api/view-interaction-end!)))

View File

@ -16,15 +16,12 @@
[app.common.geom.shapes :as gsh]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.viewport-wasm :as dwvw]
[app.main.streams :as ms]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn- render-context-lost?
[state]
(true? (get-in state [:render-state :lost])))
(defn impl-update-zoom
[{:keys [vbox] :as local} center zoom]
(let [new-zoom (if (fn? zoom) (zoom (:zoom local)) zoom)
@ -47,11 +44,15 @@
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(let [center (if (= center ::auto) @ms/mouse-position center)]
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (min (* z 1.3) 200))))))))))
#(impl-update-zoom % center (fn [z] (min (* z 1.3) 200)))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state)))))
(defn decrease-zoom
([]
@ -62,11 +63,15 @@
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(let [center (if (= center ::auto) @ms/mouse-position center)]
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))))))
#(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01)))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state)))))
(defn set-zoom
([scale]
@ -77,7 +82,7 @@
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(let [vp (dm/get-in state [:workspace-local :vbox])
x (+ (:x vp) (/ (:width vp) 2))
@ -86,22 +91,30 @@
(update state :workspace-local
#(impl-update-zoom % center (fn [z] (-> (* z scale)
(max 0.01)
(min 200)))))))))))
(min 200))))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state)))))
(def reset-zoom
(ptk/reify ::reset-zoom
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(update state :workspace-local
#(impl-update-zoom % nil 1))))))
#(impl-update-zoom % nil 1))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(def zoom-to-fit-all
(ptk/reify ::zoom-to-fit-all
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
@ -116,13 +129,17 @@
(-> local
(assoc :zoom zoom)
(assoc :zoom-inverse (/ 1 zoom))
(update :vbox merge srect)))))))))))
(update :vbox merge srect)))))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(def zoom-to-selected-shape
(ptk/reify ::zoom-to-selected-shape
ptk/UpdateEvent
(update [_ state]
(if (render-context-lost? state)
(if (dwvw/render-context-lost? state)
state
(let [selected (dsh/lookup-selected state)]
(if (empty? selected)
@ -139,14 +156,18 @@
(-> local
(assoc :zoom zoom)
(assoc :zoom-inverse (/ 1 zoom))
(update :vbox merge srect))))))))))))
(update :vbox merge srect))))))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(defn fit-to-shapes
[ids]
(ptk/reify ::fit-to-shapes
ptk/UpdateEvent
(update [_ state]
(if (or (render-context-lost? state) (empty? ids))
(if (or (dwvw/render-context-lost? state) (empty? ids))
state
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
@ -164,16 +185,21 @@
(-> local
(assoc :zoom zoom)
(assoc :zoom-inverse (/ 1 zoom))
(update :vbox merge srect))))))))))
(update :vbox merge srect))))))))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-sync-workspace-local-viewport! state))))
(defn start-zooming [pt]
(ptk/reify ::start-zooming
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (->> stream (rx/filter (ptk/type? ::finish-zooming)))]
(when (and (not (render-context-lost? state))
(when (and (not (dwvw/render-context-lost? state))
(not (get-in state [:workspace-local :zooming])))
(rx/concat
(rx/of (fn [s] (dwvw/maybe-view-interaction-start! s) s))
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
(->> stream
(rx/filter mse/pointer-event?)
@ -189,4 +215,7 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :zooming)))))
(update :workspace-local dissoc :zooming)))
ptk/EffectEvent
(effect [_ state _]
(dwvw/maybe-view-interaction-end! state))))

View File

@ -77,7 +77,8 @@
(mf/deps vport)
(fn [resize-type size]
(when (and vport (not= size vport))
(st/emit! (dw/update-viewport-size resize-type size)))))
(st/emit! (dw/update-viewport-size resize-type size)
(dw/sync-wasm-workspace-viewport)))))
on-resize-palette
(mf/use-fn

View File

@ -436,7 +436,8 @@
(mf/with-effect [vport]
(when (and @canvas-init? @initialized?)
(wasm.api/resize-viewbox (:width vport) (:height vport))))
(wasm.api/resize-viewbox (:width vport) (:height vport))
(wasm.api/set-view-box zoom vbox)))
(mf/with-effect [@canvas-init? preview-blend]
(when (and @canvas-init? preview-blend)
@ -467,10 +468,6 @@
(wasm.api/clear-focus-mode)
(wasm.api/set-focus-mode focus)))))
(mf/with-effect [vbox zoom]
(when (and @canvas-init? @initialized?)
(wasm.api/set-view-box zoom vbox)))
(mf/with-effect [background]
(when (and @canvas-init? @initialized?)
(wasm.api/set-canvas-background background)))

View File

@ -208,6 +208,8 @@
(def ^:const DEBOUNCE_DELAY_MS 100)
(defonce ^:private view-interaction-active? (atom false))
;; Time budget (ms) per chunk of shape processing before yielding to browser
(def ^:private ^:const CHUNK_TIME_BUDGET_MS 8)
;; Threshold below which we use synchronous processing (no chunking overhead)
@ -1146,14 +1148,26 @@
(= result 1))
false))
(defn view-interaction-start!
[]
(when-not @view-interaction-active?
(h/call wasm/internal-module "_set_view_start")
(reset! view-interaction-active? true)))
(defn view-interaction-end!
[]
(when @view-interaction-active?
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(perf/end-measure "render-finish")
(reset! view-interaction-active? false)))
(def render-finish
(letfn [(do-render []
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(perf/end-measure "render-finish")
(view-interaction-end!)
;; Use async _render: visible tiles render synchronously
;; (no yield), interest-area tiles render progressively
;; via rAF. _set_view_end already rebuilt the tile
@ -1167,7 +1181,7 @@
(defn set-view-box
[zoom vbox]
(perf/begin-measure "set-view-box")
(h/call wasm/internal-module "_set_view_start")
(view-interaction-start!)
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
(perf/end-measure "set-view-box")
@ -1176,6 +1190,16 @@
(render-finish)
(perf/end-measure "render-from-cache"))
(defn sync-workspace-local-viewport!
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."
[state]
(when (and wasm/context-initialized?
(not @wasm/context-lost?))
(let [zoom (get-in state [:workspace-local :zoom])
vbox (get-in state [:workspace-local :vbox])]
(when (and zoom vbox)
(set-view-box zoom vbox)))))
(defn- ensure-text-content
"Guarantee that the shape always sends a valid text tree to WASM. When the
content is nil (freshly created text) we fall back to
@ -1344,6 +1368,7 @@
;; Rebuild the tile index so _render knows which shapes
;; map to which tiles after a page switch.
(h/call wasm/internal-module "_set_view_end")
(reset! view-interaction-active? false)
;; Text layouts must run after _end_loading (they
;; depend on state that is only correct when loading
@ -1402,6 +1427,7 @@
;; Rebuild the tile index so _render knows which shapes
;; map to which tiles after a page switch.
(h/call wasm/internal-module "_set_view_end")
(reset! view-interaction-active? false)
(process-pending shapes thumbnails full
(fn []
(if render-callback