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)))
(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))
pending-thumbnails (into [] (concat
text-content-pending
@ -1616,12 +1618,13 @@
(set-shape-strokes id strokes false write-fills-strokes?)))]
{:thumbnails pending-thumbnails
: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
[shape]
(if-not (and shape (wasm/live?))
{:thumbnails [] :full [] :font-pending-ids []}
{:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}}
(do
(perf/begin-measure "set-object")
(let [shape (svg-filters/apply-svg-derived shape)]
@ -1629,6 +1632,51 @@
(let [result (set-object-host-attrs shape false)]
(perf/end-measure "set-object")
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
"Synchronously update text layouts for all shapes and send rect updates
to the worker index."
@ -1697,20 +1745,22 @@
(defn- relayout-after-fonts!
"Relayout text shapes once their pending fonts have resolved. Font fetches
are deduped per URL and storing a font does not invalidate cached layouts,
so every text shape (not only the fetch triggers in `font-pending-ids`)
needs a forced relayout; then re-sync selrects that drifted."
[shapes font-pending-ids]
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
are deduped per URL, so only shapes that use a not-yet-ready face at upload
time need a forced relayout; then re-sync selrects that drifted for those
shapes only."
[shapes text-font-state]
(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)
(if (seq font-pending-ids)
(do
(force-update-text-layouts text-ids)
(sync-stale-text-selrects! shapes))
(if (seq affected-ids)
(let [affected-shapes (into [] (keep shapes-by-id) affected-ids)]
(force-update-text-layouts affected-ids)
(sync-stale-text-selrects! affected-shapes))
(update-text-layouts text-ids)))))
(defn process-pending
[shapes thumbnails full font-pending-ids on-complete]
[shapes thumbnails full text-font-state on-complete]
(let [pending-thumbnails
(d/index-by :key :callback thumbnails)
@ -1736,7 +1786,7 @@
noop-fn
noop-fn
(fn []
(relayout-after-fonts! shapes font-pending-ids)
(relayout-after-fonts! shapes text-font-state)
(request-render "images-loaded")
(when (fn? on-complete) (on-complete)))))
;; No pending images — complete immediately.
@ -1744,8 +1794,13 @@
(defn process-object
[shape]
(let [{:keys [thumbnails full font-pending-ids]} (set-object shape)]
(process-pending [shape] thumbnails full font-pending-ids noop-fn)))
(let [{:keys [thumbnails full font-face-keys pending-font-face-keys]}
(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
"Like process-object but for multiple shapes at once. Accumulates all
@ -1754,19 +1809,26 @@
just the first shape that triggered the fetch."
[shapes]
(let [total-shapes (count shapes)
{:keys [thumbnails full font-pending-ids]}
(loop [index 0 thumbnails-acc (transient []) full-acc (transient []) font-acc (transient [])]
{:keys [thumbnails full text-font-state]}
(loop [index 0
thumbnails-acc (transient [])
full-acc (transient [])
font-state-acc empty-text-font-state]
(if (< index total-shapes)
(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)
(reduce conj! thumbnails-acc thumbnails)
(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)
:full (persistent! full-acc)
:font-pending-ids (persistent! font-acc)}))]
(process-pending shapes thumbnails full font-pending-ids noop-fn)))
:text-font-state font-state-acc}))]
(process-pending shapes thumbnails full text-font-state noop-fn)))
(def ^:private ^:const BATCH_MAX_SHAPES 512)
@ -1776,8 +1838,8 @@
Structural attrs are uploaded in one `_set_shapes_batch` FFI per chunk;
host-specific attrs (fills/strokes/text/grid/path) stay per-shape.
Returns {:thumbnails [...] :full [...] :font-pending-ids [...] :next-index n}"
[shapes start-index thumbnails-acc full-acc font-pending-acc]
Returns {:thumbnails [...] :full [...] :text-font-state {...} :next-index n}"
[shapes start-index thumbnails-acc full-acc text-font-state-acc]
(let [total (count shapes)
end-index (min total (+ start-index BATCH_MAX_SHAPES))
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
@ -1804,17 +1866,20 @@
(loop [xs prepared
t-acc (transient thumbnails-acc)
f-acc (transient full-acc)
fp-acc (transient font-pending-acc)]
font-state-acc text-font-state-acc]
(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)]
(recur (next xs)
(reduce conj! t-acc thumbnails)
(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)
:full (persistent! f-acc)
:font-pending-ids (persistent! fp-acc)
:text-font-state font-state-acc
:next-index end-index}))))
(defn- set-objects-async
@ -1825,16 +1890,16 @@
(let [total-shapes (count shapes)]
(p/create
(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)
;; 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
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)
(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
(do
(perf/end-measure "set-objects")
@ -1888,9 +1953,9 @@
noop-fn
noop-fn
(fn []
(relayout-after-fonts! shapes font-pending-acc)
(relayout-after-fonts! shapes text-font-state-acc)
(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
@ -1957,22 +2022,25 @@
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
(props/set-shape-path-content (get shape :content))))))
(let [total-shapes (count prepared)
{:keys [thumbnails full font-pending-ids]}
{:keys [thumbnails full text-font-state]}
(loop [index 0
thumbnails-acc (transient [])
full-acc (transient [])
font-acc (transient [])]
font-state-acc empty-text-font-state]
(if (< index total-shapes)
(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)]
(recur (inc index)
(reduce conj! thumbnails-acc thumbnails)
(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)
:full (persistent! full-acc)
:font-pending-ids (persistent! font-acc)}))]
:text-font-state font-state-acc}))]
(perf/end-measure "set-objects")
(when on-shapes-ready (on-shapes-ready))
(when (wasm/live?)
@ -1980,7 +2048,7 @@
;; 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 font-pending-ids
(process-pending shapes thumbnails full text-font-state
(fn []
(if render-callback
(render-callback)

View File

@ -252,12 +252,12 @@
(let [text-content-pending (api/set-shape-text-content id v)
pending-thumbnails (vec text-content-pending)
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
;; asynchronously we should probably modify set-wasm-attr!
;; to return a list of callbacks to be executed in a
;; 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))
:grow-type