mirror of
https://github.com/penpot/penpot.git
synced 2026-08-19 19:28:36 +00:00
* ✨ Add component synchronization to waitForLayoutUpdate * 🐛 Fix async mock leak in workspace-reflow-test Use mock/with-mocks instead of with-redefs for http/send! mock in failed-google-font-css-does-not-abort-shared-consumers test. with-redefs restores bindings when the block exits synchronously, but the RxJS subscription fires asynchronously. This caused the mock to leak into subsequent tests (workspace-media-test), producing 3 spurious failures. AI-assisted-by: mimo-v2.5-pro * ♻️ Replace async with-redefs with mock/with-mocks in frontend tests with-redefs restores bindings when the block exits synchronously, which is too early for async code (t/async, rx/subs!, promises). mock/with-mocks uses set! and restores in the done callback, keeping mocks alive across async boundaries. Converted 15 with-redefs usages across 4 test files: - workspace_reflow_test.cljs: 2 genuinely async tests (P1) - routes_test.cljs: 3 SSO caching tests (P2) - main_errors_test.cljs: 8 expired-org SSO tests (P2) - comments_test.cljs: 2 comment thread tests (P2) 36 purely sync with-redefs usages left unchanged — with-redefs is correct for synchronous code. AI-assisted-by: mimo-v2.5-pro * 📎 Fix fmt issues --------- Co-authored-by: Andrey Antukh <niwi@niwi.nz>
265 lines
11 KiB
Clojure
265 lines
11 KiB
Clojure
;; 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 Sucursal en España SL
|
|
|
|
(ns app.main.data.workspace.wasm-text
|
|
"Helpers/events to resize wasm text shapes without depending on workspace.texts.
|
|
|
|
This exists to avoid circular deps:
|
|
workspace.texts -> workspace.libraries -> workspace.texts"
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.geom.matrix :as gmt]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.types.modifiers :as ctm]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.workspace :as-alias dw]
|
|
[app.main.data.workspace.modifiers :as dwm]
|
|
[app.main.data.workspace.reflow :as wrf]
|
|
[app.main.data.workspace.shapes :as dwsh]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.render-wasm.api.fonts :as wasm.fonts]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(def debounce-resize-text-time 40)
|
|
|
|
(defn get-wasm-text-new-size
|
|
"Computes the new {width, height} for a text shape from WASM text layout.
|
|
For :fixed grow-type, updates WASM content and returns current dimensions (no resize)."
|
|
([shape]
|
|
(get-wasm-text-new-size shape (:content shape)))
|
|
|
|
([{:keys [id selrect grow-type] :as shape} content]
|
|
;; 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)
|
|
;; While the WASM text editor is actively editing it already holds the live
|
|
;; content and layout. Re-pushing the content here calls `_clear_shape_text`
|
|
;; + `_update_shape_text_layout`, which resets the editor and drops every
|
|
;; keystroke after the first, so we just measure the live layout instead.
|
|
(when-not (and (wasm.api/text-editor-has-focus?)
|
|
(= id (wasm.api/text-editor-get-active-shape-id)))
|
|
(wasm.api/set-shape-text-content id content)
|
|
(wasm.api/set-shape-text-images id content))
|
|
(let [dimension (when (not= :fixed grow-type)
|
|
(wasm.api/get-text-dimensions))]
|
|
;; nil dimension = shape not present in WASM state; skip the resize.
|
|
(when (or (= :fixed grow-type) (some? dimension))
|
|
{:width (if (#{:fixed :auto-height} grow-type)
|
|
(:width selrect)
|
|
(:width dimension))
|
|
:height (if (= :fixed grow-type)
|
|
(:height selrect)
|
|
(:height dimension))})))))
|
|
|
|
(defn resize-wasm-text-modifiers
|
|
([shape]
|
|
(resize-wasm-text-modifiers shape (:content shape)))
|
|
|
|
([{:keys [id points selrect] :as shape} content]
|
|
(when-let [new-size (get-wasm-text-new-size shape content)]
|
|
(let [width-scale (/ (:width new-size) (:width selrect))
|
|
height-scale (/ (:height new-size) (:height selrect))
|
|
resize-v (gpt/point width-scale height-scale)
|
|
origin (first points)]
|
|
{id
|
|
{:modifiers
|
|
(ctm/resize-modifiers
|
|
resize-v
|
|
origin
|
|
(:transform shape (gmt/matrix))
|
|
(:transform-inverse shape (gmt/matrix)))}}))))
|
|
|
|
(defn resize-wasm-text
|
|
"Resize a single text shape (auto-width/auto-height) by id.
|
|
No-op if the id is not a text shape or is :fixed."
|
|
[id]
|
|
(ptk/reify ::resize-wasm-text
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
resize-stream
|
|
(if (and (some? shape)
|
|
(cfh/text-shape? shape)
|
|
(not= :fixed (:grow-type shape)))
|
|
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
|
|
(rx/empty))]
|
|
(wrf/with-pending :text-resize [id] resize-stream)))))
|
|
|
|
(defn resize-wasm-text-debounce-commit
|
|
([]
|
|
(resize-wasm-text-debounce-commit nil nil))
|
|
([undo-group undo-id]
|
|
(ptk/reify ::resize-wasm-text-debounce-commit
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [ids (get state ::resize-wasm-text-debounce-ids)
|
|
objects (dsh/lookup-page-objects state)
|
|
|
|
modifiers
|
|
(reduce
|
|
(fn [modifiers id]
|
|
(let [shape (get objects id)]
|
|
(cond-> modifiers
|
|
(and (some? shape)
|
|
(cfh/text-shape? shape)
|
|
(not= :fixed (:grow-type shape)))
|
|
(merge (resize-wasm-text-modifiers shape)))))
|
|
{}
|
|
ids)
|
|
|
|
;; When undo-id is present, extend the current undo transaction instead of
|
|
;; creating a new one, and commit it after the resize (single undo action).
|
|
extend-tx? (some? undo-id)
|
|
apply-opts (cond-> {}
|
|
(some? undo-group) (assoc :undo-group undo-group)
|
|
extend-tx? (assoc :undo-transation? false))]
|
|
(cond
|
|
(not (empty? modifiers))
|
|
(if extend-tx?
|
|
(rx/concat
|
|
(rx/of (dwm/apply-wasm-modifiers modifiers apply-opts))
|
|
(rx/of (dwu/commit-undo-transaction undo-id)))
|
|
(rx/of (dwm/apply-wasm-modifiers modifiers apply-opts)))
|
|
|
|
extend-tx?
|
|
;; No resize needed (e.g. :fixed grow-type) but we must commit the add
|
|
(rx/of (dwu/commit-undo-transaction undo-id))
|
|
|
|
:else
|
|
(rx/empty)))))))
|
|
|
|
;; This event will debounce the resize events so, if there are many, they
|
|
;; are processed at the same time and not one-by-one. This will improve
|
|
;; performance because it's better to make only one layout calculation instead
|
|
;; of (potentialy) hundreds.
|
|
(defn resize-wasm-text-debounce-inner
|
|
([id]
|
|
(resize-wasm-text-debounce-inner id nil))
|
|
([id {:keys [undo-group undo-id]}]
|
|
(let [cur-event (js/Symbol)
|
|
reflow-task (wrf/task :text-resize [id])]
|
|
(ptk/reify ::resize-wasm-text-debounce-inner
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(update ::resize-wasm-text-debounce-ids (fnil conj []) id)
|
|
(update ::resize-wasm-text-reflow-tasks (fnil conj []) reflow-task)
|
|
(cond-> (nil? (::resize-wasm-text-debounce-event state))
|
|
(assoc ::resize-wasm-text-debounce-event cur-event))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(wrf/start! reflow-task)
|
|
(if (= (::resize-wasm-text-debounce-event state) cur-event)
|
|
(let [stopper (->> stream (rx/filter (ptk/type? ::dw/finalize-workspace)))]
|
|
(rx/concat
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::resize-wasm-text-debounce-inner))
|
|
(rx/debounce debounce-resize-text-time)
|
|
(rx/take 1)
|
|
(rx/map (fn [evt]
|
|
(resize-wasm-text-debounce-commit
|
|
(some-> evt meta :undo-group)
|
|
(some-> evt meta :undo-id))))
|
|
(rx/take-until stopper))
|
|
(rx/of (with-meta
|
|
(resize-wasm-text-debounce-inner id)
|
|
{:undo-group undo-group :undo-id undo-id})))
|
|
;; Cleanup, reached both after the commit and when the stopper
|
|
;; cancels the debounce, so the batch always drains and stays
|
|
;; pending until the resize is applied. All exact tasks in the
|
|
;; batch are retained in state and finished by the cleanup.
|
|
(rx/of (fn [state]
|
|
(wrf/finish-tasks! (::resize-wasm-text-reflow-tasks state))
|
|
(dissoc state
|
|
::resize-wasm-text-debounce-ids
|
|
::resize-wasm-text-reflow-tasks
|
|
::resize-wasm-text-debounce-event)))))
|
|
(rx/empty)))))))
|
|
|
|
(defn resize-wasm-text-debounce
|
|
([id]
|
|
(resize-wasm-text-debounce id nil))
|
|
([id {:keys [undo-group undo-id] :as opts}]
|
|
(ptk/reify ::resize-wasm-text-debounce
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
content (dm/get-in objects [id :content])
|
|
fonts (wasm.fonts/get-content-fonts content)
|
|
|
|
fonts-ready?
|
|
(->> fonts
|
|
(every?
|
|
(fn [font]
|
|
(let [font-data (wasm.fonts/make-font-data font)]
|
|
(wasm.fonts/font-ready? font-data)))))
|
|
|
|
resize-wasm-stream
|
|
(if fonts-ready?
|
|
(let [pass-opts (when (or (some? undo-group) (some? undo-id))
|
|
(cond-> {}
|
|
(some? undo-group) (assoc :undo-group undo-group)
|
|
(some? undo-id) (assoc :undo-id undo-id)))]
|
|
(rx/of (resize-wasm-text-debounce-inner id pass-opts)))
|
|
|
|
;; Fonts not loaded; retry after 20 msecs
|
|
(->> (rx/of (resize-wasm-text-debounce id opts))
|
|
(rx/delay 20)))]
|
|
|
|
;; Holds the shape pending across the font-retry loop: the retried
|
|
;; event opens its task before this one drains, so the task set never
|
|
;; becomes empty in between.
|
|
(wrf/with-pending :text-resize [id] resize-wasm-stream))))))
|
|
|
|
(defn resize-wasm-text-all
|
|
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
|
([ids]
|
|
(resize-wasm-text-all ids nil))
|
|
([ids opts]
|
|
(ptk/reify ::resize-wasm-text-all
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(let [resize-stream
|
|
(->> (rx/from ids)
|
|
(rx/map #(resize-wasm-text-debounce % opts)))
|
|
|
|
buffer-finished-stream
|
|
(->> (rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dwsh/update-shapes-buffer-commit))
|
|
(rx/map (constantly :commit)))
|
|
;; Let a buffered commit beat the stop signal.
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dwsh/update-shapes-buffer-stop))
|
|
(rx/observe-on :async)
|
|
(rx/map (constantly :stop)))
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dw/finalize-workspace))
|
|
(rx/map (constantly :finalize))))
|
|
(rx/take 1))]
|
|
(if (::dwsh/update-shapes-buffer state)
|
|
;; If we're in the middle of a token propagation we wait until is finished to
|
|
;; recalculate the text sizes. The shapes stay pending for that whole wait,
|
|
;; since the per-shape debounce only marks them once dispatched.
|
|
(wrf/with-pending
|
|
:text-resize ids
|
|
(->> buffer-finished-stream
|
|
(rx/mapcat
|
|
(fn [reason]
|
|
(if (= reason :finalize)
|
|
(rx/empty)
|
|
resize-stream)))))
|
|
resize-stream))))))
|