mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 17:02:05 +00:00
🐛 Fix text editor crash when switching from svg to wasm renderer (#9926)
* 🐛 Fix crash when switching renderers with text editor open * ♻️ Use new initialized? helper in wasm api
This commit is contained in:
parent
b5108ca1ad
commit
a5c8bcaf9e
@ -35,6 +35,7 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.text-editor :as wasm.text-editor]
|
||||
[app.util.text-editor :as ted]
|
||||
@ -83,17 +84,23 @@
|
||||
[]
|
||||
(ptk/reify ::focus-editor
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [editor (:workspace-editor state)
|
||||
element (when editor (.-element editor))]
|
||||
(cond
|
||||
;; V1 (DraftEditor)
|
||||
(.-focus editor)
|
||||
(ts/schedule #(.focus ^js editor))
|
||||
(effect [_ _ _]
|
||||
;; The focus is deferred, so we re-read the current editor at fire
|
||||
;; time: the editor present now can be unmounted before the timeout
|
||||
;; runs (e.g. switching renderer while editing a text), and focusing a
|
||||
;; stale instance throws.
|
||||
(ts/schedule
|
||||
(fn []
|
||||
(let [editor (:workspace-editor @st/state)
|
||||
element (when editor (.-element editor))]
|
||||
(cond
|
||||
;; V1 (DraftEditor)
|
||||
(and (some? editor) (.-focus editor))
|
||||
(.focus ^js editor)
|
||||
|
||||
;; V2
|
||||
(and element (.-focus element))
|
||||
(ts/schedule #(.focus ^js element)))))))
|
||||
;; V2
|
||||
(and element (.-focus element))
|
||||
(.focus ^js element))))))))
|
||||
|
||||
(defn gen-name
|
||||
[editor]
|
||||
|
||||
@ -33,7 +33,10 @@
|
||||
(get-wasm-text-new-size shape (:content shape)))
|
||||
|
||||
([{:keys [id selrect grow-type] :as shape} content]
|
||||
(when id
|
||||
;; Skip when the WASM context is not ready (e.g. switching renderer while a
|
||||
;; text shape is being edited): there is no design state to query, and
|
||||
;; returning nil makes callers skip the WASM resize/modifier path.
|
||||
(when (and id (wasm.api/initialized?))
|
||||
(wasm.api/use-shape id)
|
||||
(wasm.api/set-shape-text-content id content)
|
||||
(wasm.api/set-shape-text-images id content)
|
||||
|
||||
@ -85,28 +85,34 @@
|
||||
parents
|
||||
(mf/deref parents-by-ids-ref)
|
||||
|
||||
;; Deref every editor-state ref unconditionally so the hook order
|
||||
;; stays stable when feature flags change at runtime (e.g. switching
|
||||
;; renderer while editing a text). The selection happens afterwards.
|
||||
wasm-editor-styles-map (mf/deref refs/workspace-wasm-editor-styles)
|
||||
v2-editor-state-map (mf/deref refs/workspace-v2-editor-state)
|
||||
v1-editor-state-map (mf/deref refs/workspace-editor-state)
|
||||
editor (mf/deref refs/workspace-editor)
|
||||
|
||||
text-editor-wasm? (features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
text-editor-v2? (features/active-feature? @st/state "text-editor/v2")
|
||||
|
||||
state-map
|
||||
(cond
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(mf/deref refs/workspace-wasm-editor-styles)
|
||||
|
||||
(features/active-feature? @st/state "text-editor/v2")
|
||||
(mf/deref refs/workspace-v2-editor-state)
|
||||
|
||||
:else
|
||||
(mf/deref refs/workspace-editor-state))
|
||||
text-editor-wasm? wasm-editor-styles-map
|
||||
text-editor-v2? v2-editor-state-map
|
||||
:else v1-editor-state-map)
|
||||
|
||||
editor-styles
|
||||
(when (features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(when text-editor-wasm?
|
||||
(get state-map id))
|
||||
|
||||
editor-state
|
||||
(when (not (features/active-feature? @st/state "text-editor/v2"))
|
||||
(when (not text-editor-v2?)
|
||||
(get state-map id))
|
||||
|
||||
editor-instance
|
||||
(when (features/active-feature? @st/state "text-editor/v2")
|
||||
(mf/deref refs/workspace-editor))
|
||||
(when text-editor-v2?
|
||||
editor)
|
||||
|
||||
fill-values
|
||||
(dwt/current-text-values
|
||||
|
||||
@ -424,9 +424,9 @@
|
||||
(js/clearTimeout timeout-id))
|
||||
(wasm.api/clear-canvas)))))
|
||||
|
||||
(mf/with-effect [show-text-editor? workspace-editor-state edition]
|
||||
(mf/with-effect [show-text-editor? workspace-editor-state edition @canvas-init? @initialized?]
|
||||
(let [active-editor-state (get workspace-editor-state edition)]
|
||||
(when (and show-text-editor? active-editor-state)
|
||||
(when (and show-text-editor? active-editor-state @canvas-init? @initialized?)
|
||||
(let [content (-> active-editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content))]
|
||||
|
||||
@ -85,6 +85,15 @@
|
||||
|
||||
(def ^:private snapshot-capture-debounce-ms 250)
|
||||
|
||||
|
||||
(defn initialized?
|
||||
"True when the WASM render context is ready to receive design-state
|
||||
operations. Use it to skip WASM work during transient states (e.g. while
|
||||
switching renderer with a text shape being edited)."
|
||||
[]
|
||||
(and wasm/context-initialized? (not @wasm/context-lost?)))
|
||||
|
||||
|
||||
(defn set-transition-image-from-background!
|
||||
"Sets `transition-image-url*` to a data URL representing a solid background color."
|
||||
[background]
|
||||
@ -161,8 +170,7 @@
|
||||
(defonce ^:private schedule-canvas-snapshot-capture!
|
||||
(fns/debounce
|
||||
(fn []
|
||||
(when (and wasm/context-initialized?
|
||||
(not @wasm/context-lost?)
|
||||
(when (and (initialized?)
|
||||
(some? wasm/canvas))
|
||||
(-> (webgl/capture-canvas-snapshot-url)
|
||||
(p/catch (fn [_] nil)))))
|
||||
@ -323,14 +331,13 @@
|
||||
[]
|
||||
;; check if the context has not been lost already or we will get warnings about
|
||||
;; removing objects from a non-current context
|
||||
(when (and wasm/context-initialized?
|
||||
(not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_free_gpu_resources")))
|
||||
|
||||
;; This should never be called from the outside.
|
||||
(defn- render
|
||||
[timestamp]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_render" timestamp)
|
||||
|
||||
;; Update text editor blink (so cursor toggles) using the same timestamp
|
||||
@ -361,13 +368,13 @@
|
||||
|
||||
(defn render-sync
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_render_sync")
|
||||
(set! wasm/internal-frame-id nil)))
|
||||
|
||||
(defn render-sync-shape
|
||||
[id]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_render_sync_shape"
|
||||
(aget buffer 0)
|
||||
@ -380,7 +387,7 @@
|
||||
"Render a lightweight preview without tile caching.
|
||||
Used during progressive loading for fast feedback."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_render_preview")))
|
||||
|
||||
|
||||
@ -394,7 +401,7 @@
|
||||
|
||||
(defn request-render
|
||||
[_requester]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?) (not @wasm/disable-request-render?))
|
||||
(when (and (initialized?) (not @wasm/disable-request-render?))
|
||||
(if @shapes-loading?
|
||||
(register-deferred-render!)
|
||||
(when-not @pending-render
|
||||
@ -431,7 +438,7 @@
|
||||
|
||||
(defn use-shape
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(when (initialized?)
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_use_shape"
|
||||
(aget buffer 0)
|
||||
@ -441,7 +448,7 @@
|
||||
|
||||
(defn has-shape
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(when (initialized?)
|
||||
(let [buffer (uuid/get-u32 id)
|
||||
|
||||
result
|
||||
@ -459,17 +466,20 @@
|
||||
;; Cache content for text editor sync
|
||||
(text-editor/cache-shape-text-content! shape-id content)
|
||||
|
||||
(h/call wasm/internal-module "_clear_shape_text")
|
||||
;; The WASM design state may not be ready (e.g. while switching renderer
|
||||
;; with a text shape being edited). Skip the WASM layout calls in that case.
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_clear_shape_text")
|
||||
|
||||
(set-shape-vertical-align (get content :vertical-align))
|
||||
(set-shape-vertical-align (get content :vertical-align))
|
||||
|
||||
(let [fonts (f/get-content-fonts content)
|
||||
fallback-fonts (fonts-from-text-content content true)
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts all-fonts)]
|
||||
(f/load-fallback-fonts-for-editor! fallback-fonts)
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result))
|
||||
(let [fonts (f/get-content-fonts content)
|
||||
fallback-fonts (fonts-from-text-content content true)
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts all-fonts)]
|
||||
(f/load-fallback-fonts-for-editor! fallback-fonts)
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result)))
|
||||
|
||||
(defn apply-styles-to-selection
|
||||
"Apply style attrs to the currently selected text spans.
|
||||
@ -1130,21 +1140,23 @@
|
||||
(use-shape id)
|
||||
(get-text-dimensions))
|
||||
([]
|
||||
(let [offset (-> (h/call wasm/internal-module "_get_text_dimensions")
|
||||
(mem/->offset-32))
|
||||
heapf32 (mem/get-heap-f32)
|
||||
width (aget heapf32 (+ offset 0))
|
||||
height (aget heapf32 (+ offset 1))
|
||||
max-width (aget heapf32 (+ offset 2))
|
||||
(if-not (initialized?)
|
||||
{:x 0 :y 0 :width 0 :height 0 :max-width 0}
|
||||
(let [offset (-> (h/call wasm/internal-module "_get_text_dimensions")
|
||||
(mem/->offset-32))
|
||||
heapf32 (mem/get-heap-f32)
|
||||
width (aget heapf32 (+ offset 0))
|
||||
height (aget heapf32 (+ offset 1))
|
||||
max-width (aget heapf32 (+ offset 2))
|
||||
|
||||
x (aget heapf32 (+ offset 3))
|
||||
y (aget heapf32 (+ offset 4))]
|
||||
(mem/free)
|
||||
{:x x :y y :width width :height height :max-width max-width})))
|
||||
x (aget heapf32 (+ offset 3))
|
||||
y (aget heapf32 (+ offset 4))]
|
||||
(mem/free)
|
||||
{:x x :y y :width width :height height :max-width max-width}))))
|
||||
|
||||
(defn intersect-position-in-shape
|
||||
[id position]
|
||||
(if (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(if (initialized?)
|
||||
(let [buffer (uuid/get-u32 id)
|
||||
result
|
||||
(h/call wasm/internal-module "_intersect_position_in_shape"
|
||||
@ -1175,7 +1187,7 @@
|
||||
(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?))
|
||||
(when (initialized?)
|
||||
(view-interaction-end!)
|
||||
;; Use async _render: visible tiles render synchronously
|
||||
;; (no yield), interest-area tiles render progressively
|
||||
@ -1202,8 +1214,7 @@
|
||||
(defn sync-workspace-local-viewport!
|
||||
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."
|
||||
[state]
|
||||
(when (and wasm/context-initialized?
|
||||
(not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(let [zoom (get-in state [:workspace-local :zoom])
|
||||
vbox (get-in state [:workspace-local :vbox])]
|
||||
(when (and zoom vbox)
|
||||
@ -1661,7 +1672,8 @@
|
||||
|
||||
(defn clean-modifiers
|
||||
[]
|
||||
(h/call wasm/internal-module "_clean_modifiers"))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_clean_modifiers")))
|
||||
|
||||
(defn set-modifiers-start
|
||||
"Enter interactive transform mode (drag / resize / rotate). Enables
|
||||
@ -1669,7 +1681,7 @@
|
||||
backdrop so tiles do not appear sequentially or flicker while the
|
||||
gesture is in progress."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_set_modifiers_start")))
|
||||
|
||||
(defn set-modifiers-end
|
||||
@ -1677,7 +1689,7 @@
|
||||
scheduled under it; the caller is expected to trigger a full-quality
|
||||
render (via `request-render`) once the gesture is committed."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(when (initialized?)
|
||||
(h/call wasm/internal-module "_set_modifiers_end")))
|
||||
|
||||
(defn set-modifiers
|
||||
@ -2164,7 +2176,7 @@
|
||||
|
||||
(defn calculate-position-data
|
||||
[shape]
|
||||
(when wasm/context-initialized?
|
||||
(when (initialized?)
|
||||
(use-shape (:id shape))
|
||||
(let [heapf32 (mem/get-heap-f32)
|
||||
heapu32 (mem/get-heap-u32)
|
||||
|
||||
@ -85,6 +85,31 @@
|
||||
(track! :set-shape-grow-type)
|
||||
nil)
|
||||
|
||||
(defn- mock-initialized?
|
||||
[]
|
||||
(track! :initialized?)
|
||||
true)
|
||||
|
||||
;; The functions below used to short-circuit in tests because they guarded on
|
||||
;; `wasm/context-initialized?` (always false without a real WASM binary). They
|
||||
;; now guard on `initialized?`, which the mock forces to `true`, so they would
|
||||
;; reach the real WASM heap. Stub them as no-ops to preserve that behavior.
|
||||
|
||||
(defn- mock-use-shape
|
||||
[_id]
|
||||
(track! :use-shape)
|
||||
nil)
|
||||
|
||||
(defn- mock-calculate-position-data
|
||||
[_shape]
|
||||
(track! :calculate-position-data)
|
||||
nil)
|
||||
|
||||
(defn- mock-request-render
|
||||
[_requester]
|
||||
(track! :request-render)
|
||||
nil)
|
||||
|
||||
(defn- mock-set-shape-text-content
|
||||
[_shape-id _content]
|
||||
(track! :set-shape-text-content)
|
||||
@ -143,7 +168,11 @@
|
||||
(reset-call-counts!)
|
||||
;; Save originals
|
||||
(reset! originals
|
||||
{:clean-modifiers wasm.api/clean-modifiers
|
||||
{:initialized? wasm.api/initialized?
|
||||
:use-shape wasm.api/use-shape
|
||||
:calculate-position-data wasm.api/calculate-position-data
|
||||
:request-render wasm.api/request-render
|
||||
:clean-modifiers wasm.api/clean-modifiers
|
||||
:set-structure-modifiers wasm.api/set-structure-modifiers
|
||||
:propagate-modifiers wasm.api/propagate-modifiers
|
||||
:set-modifiers wasm.api/set-modifiers
|
||||
@ -155,6 +184,10 @@
|
||||
:make-font-data wasm.fonts/make-font-data
|
||||
:get-content-fonts wasm.fonts/get-content-fonts})
|
||||
;; Install mocks
|
||||
(set! wasm.api/initialized? mock-initialized?)
|
||||
(set! wasm.api/use-shape mock-use-shape)
|
||||
(set! wasm.api/calculate-position-data mock-calculate-position-data)
|
||||
(set! wasm.api/request-render mock-request-render)
|
||||
(set! wasm.api/clean-modifiers mock-clean-modifiers)
|
||||
(set! wasm.api/set-structure-modifiers mock-set-structure-modifiers)
|
||||
(set! wasm.api/propagate-modifiers mock-propagate-modifiers)
|
||||
@ -171,6 +204,10 @@
|
||||
"Restore the original WASM functions saved by `setup-wasm-mocks!`."
|
||||
[]
|
||||
(let [orig @originals]
|
||||
(set! wasm.api/initialized? (:initialized? orig))
|
||||
(set! wasm.api/use-shape (:use-shape orig))
|
||||
(set! wasm.api/calculate-position-data (:calculate-position-data orig))
|
||||
(set! wasm.api/request-render (:request-render orig))
|
||||
(set! wasm.api/clean-modifiers (:clean-modifiers orig))
|
||||
(set! wasm.api/set-structure-modifiers (:set-structure-modifiers orig))
|
||||
(set! wasm.api/propagate-modifiers (:propagate-modifiers orig))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user