Scope post-font text relayout to pending font faces

Track which shapes use each font face at upload time and only force
relayout and stale-selrect sync for shapes tied to not-yet-ready
faces instead of every text shape on the page.
This commit is contained in:
Alejandro Alonso 2026-08-28 13:14:30 +02:00
parent 627921cb46
commit b2bd3582ae
2 changed files with 109 additions and 41 deletions

View File

@ -1604,6 +1604,8 @@
(set-layout-data shape))) (set-layout-data shape)))
(let [is-text? (= type :text) (let [is-text? (= type :text)
font-face-keys (when is-text? (text-font-face-keys content))
pending-font-face-keys (when is-text? (text-pending-font-face-keys content))
text-content-pending (when is-text? (set-shape-text-content id content)) text-content-pending (when is-text? (set-shape-text-content id content))
pending-thumbnails (into [] (concat pending-thumbnails (into [] (concat
text-content-pending text-content-pending
@ -1616,12 +1618,13 @@
(set-shape-strokes id strokes false write-fills-strokes?)))] (set-shape-strokes id strokes false write-fills-strokes?)))]
{:thumbnails pending-thumbnails {:thumbnails pending-thumbnails
:full pending-full :full pending-full
:font-pending-ids (if (some :callback text-content-pending) [id] [])}))) :font-face-keys (or font-face-keys #{})
:pending-font-face-keys (or pending-font-face-keys #{})})))
(defn set-object (defn set-object
[shape] [shape]
(if-not (and shape (wasm/live?)) (if-not (and shape (wasm/live?))
{:thumbnails [] :full [] :font-pending-ids []} {:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}}
(do (do
(perf/begin-measure "set-object") (perf/begin-measure "set-object")
(let [shape (svg-filters/apply-svg-derived shape)] (let [shape (svg-filters/apply-svg-derived shape)]
@ -1629,6 +1632,51 @@
(let [result (set-object-host-attrs shape false)] (let [result (set-object-host-attrs shape false)]
(perf/end-measure "set-object") (perf/end-measure "set-object")
result))))) result)))))
(def ^:private empty-text-font-state
{:font-index {} :pending-faces #{}})
(defn- text-font-face-keys
[content]
(into #{}
(comp (map f/make-font-data)
(map f/font-data-key))
(f/get-content-fonts content)))
(defn- text-pending-font-face-keys
"Font faces that were not WASM-ready when the shape was uploaded."
[content]
(into #{}
(comp (map f/make-font-data)
(remove f/font-ready?)
(map f/font-data-key))
(f/get-content-fonts content)))
(defn- acc-text-font-state
[{:keys [font-index pending-faces]} id font-face-keys pending-font-face-keys]
{:font-index (reduce (fn [idx face]
(update idx face (fnil conj #{}) id))
font-index
font-face-keys)
:pending-faces (into pending-faces pending-font-face-keys)})
(defn text-font-state-for-shape
"Build the font-face index for a single text shape (incremental updates)."
[shape]
(if (cfh/text-shape? shape)
(let [content (ensure-text-content (:content shape))]
(acc-text-font-state empty-text-font-state
(:id shape)
(text-font-face-keys content)
(text-pending-font-face-keys content)))
empty-text-font-state))
(defn- shape-ids-for-pending-fonts
[{:keys [font-index pending-faces]}]
(when (seq pending-faces)
(into #{}
(mapcat #(get font-index % []))
pending-faces)))
(defn- update-text-layouts (defn- update-text-layouts
"Synchronously update text layouts for all shapes and send rect updates "Synchronously update text layouts for all shapes and send rect updates
to the worker index." to the worker index."
@ -1697,20 +1745,22 @@
(defn- relayout-after-fonts! (defn- relayout-after-fonts!
"Relayout text shapes once their pending fonts have resolved. Font fetches "Relayout text shapes once their pending fonts have resolved. Font fetches
are deduped per URL and storing a font does not invalidate cached layouts, are deduped per URL, so only shapes that use a not-yet-ready face at upload
so every text shape (not only the fetch triggers in `font-pending-ids`) time need a forced relayout; then re-sync selrects that drifted for those
needs a forced relayout; then re-sync selrects that drifted." shapes only."
[shapes font-pending-ids] [shapes text-font-state]
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)] (let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)
affected-ids (or (shape-ids-for-pending-fonts text-font-state) #{})
shapes-by-id (d/index-by :id shapes)]
(when (seq text-ids) (when (seq text-ids)
(if (seq font-pending-ids) (if (seq affected-ids)
(do (let [affected-shapes (into [] (keep shapes-by-id) affected-ids)]
(force-update-text-layouts text-ids) (force-update-text-layouts affected-ids)
(sync-stale-text-selrects! shapes)) (sync-stale-text-selrects! affected-shapes))
(update-text-layouts text-ids))))) (update-text-layouts text-ids)))))
(defn process-pending (defn process-pending
[shapes thumbnails full font-pending-ids on-complete] [shapes thumbnails full text-font-state on-complete]
(let [pending-thumbnails (let [pending-thumbnails
(d/index-by :key :callback thumbnails) (d/index-by :key :callback thumbnails)
@ -1736,7 +1786,7 @@
noop-fn noop-fn
noop-fn noop-fn
(fn [] (fn []
(relayout-after-fonts! shapes font-pending-ids) (relayout-after-fonts! shapes text-font-state)
(request-render "images-loaded") (request-render "images-loaded")
(when (fn? on-complete) (on-complete))))) (when (fn? on-complete) (on-complete)))))
;; No pending images — complete immediately. ;; No pending images — complete immediately.
@ -1744,8 +1794,13 @@
(defn process-object (defn process-object
[shape] [shape]
(let [{:keys [thumbnails full font-pending-ids]} (set-object shape)] (let [{:keys [thumbnails full font-face-keys pending-font-face-keys]}
(process-pending [shape] thumbnails full font-pending-ids noop-fn))) (set-object shape)
text-font-state (acc-text-font-state empty-text-font-state
(:id shape)
font-face-keys
pending-font-face-keys)]
(process-pending [shape] thumbnails full text-font-state noop-fn)))
(defn process-objects (defn process-objects
"Like process-object but for multiple shapes at once. Accumulates all "Like process-object but for multiple shapes at once. Accumulates all
@ -1754,19 +1809,26 @@
just the first shape that triggered the fetch." just the first shape that triggered the fetch."
[shapes] [shapes]
(let [total-shapes (count shapes) (let [total-shapes (count shapes)
{:keys [thumbnails full font-pending-ids]} {:keys [thumbnails full text-font-state]}
(loop [index 0 thumbnails-acc (transient []) full-acc (transient []) font-acc (transient [])] (loop [index 0
thumbnails-acc (transient [])
full-acc (transient [])
font-state-acc empty-text-font-state]
(if (< index total-shapes) (if (< index total-shapes)
(let [shape (nth shapes index) (let [shape (nth shapes index)
{:keys [thumbnails full font-pending-ids]} (set-object shape)] {:keys [thumbnails full font-face-keys pending-font-face-keys]}
(set-object shape)]
(recur (inc index) (recur (inc index)
(reduce conj! thumbnails-acc thumbnails) (reduce conj! thumbnails-acc thumbnails)
(reduce conj! full-acc full) (reduce conj! full-acc full)
(reduce conj! font-acc font-pending-ids))) (acc-text-font-state font-state-acc
(:id shape)
font-face-keys
pending-font-face-keys)))
{:thumbnails (persistent! thumbnails-acc) {:thumbnails (persistent! thumbnails-acc)
:full (persistent! full-acc) :full (persistent! full-acc)
:font-pending-ids (persistent! font-acc)}))] :text-font-state font-state-acc}))]
(process-pending shapes thumbnails full font-pending-ids noop-fn))) (process-pending shapes thumbnails full text-font-state noop-fn)))
(def ^:private ^:const BATCH_MAX_SHAPES 512) (def ^:private ^:const BATCH_MAX_SHAPES 512)
@ -1776,8 +1838,8 @@
Structural attrs are uploaded in one `_set_shapes_batch` FFI per chunk; Structural attrs are uploaded in one `_set_shapes_batch` FFI per chunk;
host-specific attrs (fills/strokes/text/grid/path) stay per-shape. host-specific attrs (fills/strokes/text/grid/path) stay per-shape.
Returns {:thumbnails [...] :full [...] :font-pending-ids [...] :next-index n}" Returns {:thumbnails [...] :full [...] :text-font-state {...} :next-index n}"
[shapes start-index thumbnails-acc full-acc font-pending-acc] [shapes start-index thumbnails-acc full-acc text-font-state-acc]
(let [total (count shapes) (let [total (count shapes)
end-index (min total (+ start-index BATCH_MAX_SHAPES)) end-index (min total (+ start-index BATCH_MAX_SHAPES))
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes)) chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
@ -1804,17 +1866,20 @@
(loop [xs prepared (loop [xs prepared
t-acc (transient thumbnails-acc) t-acc (transient thumbnails-acc)
f-acc (transient full-acc) f-acc (transient full-acc)
fp-acc (transient font-pending-acc)] font-state-acc text-font-state-acc]
(if-let [shape (first xs)] (if-let [shape (first xs)]
(let [{:keys [thumbnails full font-pending-ids]} (let [{:keys [thumbnails full font-face-keys pending-font-face-keys]}
(set-object-host-attrs shape true :skip-fills-strokes? true)] (set-object-host-attrs shape true :skip-fills-strokes? true)]
(recur (next xs) (recur (next xs)
(reduce conj! t-acc thumbnails) (reduce conj! t-acc thumbnails)
(reduce conj! f-acc full) (reduce conj! f-acc full)
(reduce conj! fp-acc font-pending-ids))) (acc-text-font-state font-state-acc
(:id shape)
font-face-keys
pending-font-face-keys)))
{:thumbnails (persistent! t-acc) {:thumbnails (persistent! t-acc)
:full (persistent! f-acc) :full (persistent! f-acc)
:font-pending-ids (persistent! fp-acc) :text-font-state font-state-acc
:next-index end-index})))) :next-index end-index}))))
(defn- set-objects-async (defn- set-objects-async
@ -1825,16 +1890,16 @@
(let [total-shapes (count shapes)] (let [total-shapes (count shapes)]
(p/create (p/create
(fn [resolve _reject] (fn [resolve _reject]
(letfn [(process-next-chunk [index thumbnails-acc full-acc font-pending-acc] (letfn [(process-next-chunk [index thumbnails-acc full-acc text-font-state-acc]
(if (< index total-shapes) (if (< index total-shapes)
;; Process one time-budgeted chunk ;; Process one time-budgeted chunk
(let [{:keys [thumbnails full font-pending-ids next-index]} (let [{:keys [thumbnails full text-font-state next-index]}
(process-shapes-chunk shapes index (process-shapes-chunk shapes index
thumbnails-acc full-acc font-pending-acc)] thumbnails-acc full-acc text-font-state-acc)]
;; Yield to browser, then continue with next chunk ;; Yield to browser, then continue with next chunk
(-> (yield-to-browser) (-> (yield-to-browser)
(p/then (fn [_] (p/then (fn [_]
(process-next-chunk next-index thumbnails full font-pending-ids))))) (process-next-chunk next-index thumbnails full text-font-state)))))
;; All chunks done - finalize ;; All chunks done - finalize
(do (do
(perf/end-measure "set-objects") (perf/end-measure "set-objects")
@ -1888,9 +1953,9 @@
noop-fn noop-fn
noop-fn noop-fn
(fn [] (fn []
(relayout-after-fonts! shapes font-pending-acc) (relayout-after-fonts! shapes text-font-state-acc)
(request-render "images-loaded")))))))))))] (request-render "images-loaded")))))))))))]
(process-next-chunk 0 [] [] [])))))) (process-next-chunk 0 [] [] empty-text-font-state))))))
;; This is a version of process-pending that doesn't have sideffects ;; This is a version of process-pending that doesn't have sideffects
@ -1957,22 +2022,25 @@
(when (and (contains? #{:path :bool} type) (some? (get shape :content))) (when (and (contains? #{:path :bool} type) (some? (get shape :content)))
(props/set-shape-path-content (get shape :content)))))) (props/set-shape-path-content (get shape :content))))))
(let [total-shapes (count prepared) (let [total-shapes (count prepared)
{:keys [thumbnails full font-pending-ids]} {:keys [thumbnails full text-font-state]}
(loop [index 0 (loop [index 0
thumbnails-acc (transient []) thumbnails-acc (transient [])
full-acc (transient []) full-acc (transient [])
font-acc (transient [])] font-state-acc empty-text-font-state]
(if (< index total-shapes) (if (< index total-shapes)
(let [shape (nth prepared index) (let [shape (nth prepared index)
{:keys [thumbnails full font-pending-ids]} {:keys [thumbnails full font-face-keys pending-font-face-keys]}
(set-object-host-attrs shape true :skip-fills-strokes? true)] (set-object-host-attrs shape true :skip-fills-strokes? true)]
(recur (inc index) (recur (inc index)
(reduce conj! thumbnails-acc thumbnails) (reduce conj! thumbnails-acc thumbnails)
(reduce conj! full-acc full) (reduce conj! full-acc full)
(reduce conj! font-acc font-pending-ids))) (acc-text-font-state font-state-acc
(:id shape)
font-face-keys
pending-font-face-keys)))
{:thumbnails (persistent! thumbnails-acc) {:thumbnails (persistent! thumbnails-acc)
:full (persistent! full-acc) :full (persistent! full-acc)
:font-pending-ids (persistent! font-acc)}))] :text-font-state font-state-acc}))]
(perf/end-measure "set-objects") (perf/end-measure "set-objects")
(when on-shapes-ready (on-shapes-ready)) (when on-shapes-ready (on-shapes-ready))
(when (wasm/live?) (when (wasm/live?)
@ -1980,7 +2048,7 @@
;; map to which tiles after a page switch. ;; map to which tiles after a page switch.
(h/call wasm/internal-module "_set_view_end") (h/call wasm/internal-module "_set_view_end")
(reset! view-interaction-active? false) (reset! view-interaction-active? false)
(process-pending shapes thumbnails full font-pending-ids (process-pending shapes thumbnails full text-font-state
(fn [] (fn []
(if render-callback (if render-callback
(render-callback) (render-callback)

View File

@ -252,12 +252,12 @@
(let [text-content-pending (api/set-shape-text-content id v) (let [text-content-pending (api/set-shape-text-content id v)
pending-thumbnails (vec text-content-pending) pending-thumbnails (vec text-content-pending)
pending-full (vec (api/set-shape-text-images id v)) pending-full (vec (api/set-shape-text-images id v))
font-pending-ids (when (some :callback text-content-pending) [id])] text-font-state (api/text-font-state-for-shape shape)]
;; FIXME: this is a hack to process the pending tasks ;; FIXME: this is a hack to process the pending tasks
;; asynchronously we should probably modify set-wasm-attr! ;; asynchronously we should probably modify set-wasm-attr!
;; to return a list of callbacks to be executed in a ;; to return a list of callbacks to be executed in a
;; second pass. ;; second pass.
(api/process-pending [shape] pending-thumbnails pending-full font-pending-ids api/noop-fn) (api/process-pending [shape] pending-thumbnails pending-full text-font-state api/noop-fn)
nil)) nil))
:grow-type :grow-type