diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 11f94a7037..46bca2a7d5 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1788,29 +1788,48 @@ (sync-stale-text-selrects! affected-shapes)) (update-text-layouts text-ids))))) +(defn- await-pending-faces + "Completes once every face in `pending-faces` is stored or has failed. + Waits on the faces, not on fetch callbacks: a face whose URL is already + being fetched for another shape yields no callback for this one." + [pending-faces] + (if (empty? pending-faces) + (rx/empty) + (->> (rx/merge f/font-stored-stream f/font-storage-failed-stream) + (rx/filter pending-faces) + (rx/scan disj pending-faces) + (rx/filter empty?) + (rx/take 1) + (rx/ignore)))) + (defn process-pending [shapes thumbnails full text-font-state on-complete] (let [pending-thumbnails (d/index-by :key :callback thumbnails) pending-full - (d/index-by :key :callback full)] + (d/index-by :key :callback full) + + pending-faces + (:pending-faces text-font-state)] ;; Run text layouts synchronously so shapes are immediately correct. (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] (when (seq text-ids) (update-text-layouts text-ids))) - (if (or (seq pending-thumbnails) (seq pending-full)) - (->> (rx/concat - (->> (rx/from (vals pending-thumbnails)) - (rx/merge-map (fn [callback] (if (fn? callback) (callback) (rx/empty)))) - (rx/reduce conj []) - (rx/catch #(rx/empty))) - (->> (rx/from (vals pending-full)) - (rx/mapcat (fn [callback] (if (fn? callback) (callback) (rx/empty)))) - (rx/reduce conj []) - (rx/catch #(rx/empty)))) + (if (or (seq pending-thumbnails) (seq pending-full) (seq pending-faces)) + (->> (rx/merge + (rx/concat + (->> (rx/from (vals pending-thumbnails)) + (rx/merge-map (fn [callback] (if (fn? callback) (callback) (rx/empty)))) + (rx/reduce conj []) + (rx/catch #(rx/empty))) + (->> (rx/from (vals pending-full)) + (rx/mapcat (fn [callback] (if (fn? callback) (callback) (rx/empty)))) + (rx/reduce conj []) + (rx/catch #(rx/empty)))) + (await-pending-faces pending-faces)) (rx/subs! noop-fn noop-fn @@ -1818,7 +1837,7 @@ (relayout-after-fonts! shapes text-font-state) (request-render "images-loaded") (when (fn? on-complete) (on-complete))))) - ;; No pending images — complete immediately. + ;; No pending images or faces: complete immediately. (when on-complete (on-complete))))) (defn process-objects diff --git a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs index 32ea3a8756..6f05b0966a 100644 --- a/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/process_objects_test.cljs @@ -18,8 +18,10 @@ [app.common.render-wasm.mem :as mem] [app.common.render-wasm.wasm :as wasm] [app.render-wasm.api :as wasm.api] + [app.render-wasm.api.fonts :as wasm.fonts] [beicon.v2.core :as rx] - [cljs.test :as t :include-macros true])) + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as async])) ;; --------------------------------------------------------------------------- ;; Helpers @@ -111,6 +113,22 @@ (t/is (= 2 (count (:shapes @captured))) "Both shapes are in process-pending so font-load covers all of them"))) +(t/deftest ^:async process-pending-waits-for-faces-fetched-for-other-shapes + "A text whose face is already being fetched for another shape gets no fetch + callback. process-pending must still wait for that face, so the relayout + that follows uses the real font instead of the fallback." + (let [face "face-key" + shape (assoc (make-shape :text) :grow-type :fixed) + completed (atom false)] + (wasm.api/process-pending [shape] [] [] + {:font-index {face #{(:id shape)}} + :pending-faces #{face}} + #(reset! completed true)) + (await (async/settle)) + (t/is (not @completed) "the shape waits for its pending face") + (rx/push! wasm.fonts/font-stored-stream face) + (await (async/wait-for #(deref completed) "storing the face completes the wait")))) + (t/deftest empty-grid-tracks-do-not-allocate-zero-bytes (let [calls (atom []) ;; `h/call` is a macro that resolves the wasm function off the module