⚡ Speed up sidebar menus for large selections (#12028)

Reuse extracted colors for unchanged shapes and cache the text attrs
read from each text content.
This commit is contained in:
Elena Torró 2026-10-02 12:05:25 +02:00 committed by GitHub
parent c148ba5267
commit 822dbd6517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 124 additions and 43 deletions

View File

@ -1351,6 +1351,43 @@
:shape-id (:shape-id fill)
:index (:index fill)})))
(defn shape-colors
[shape file-id libraries]
(let [applied-tokens (:applied-tokens shape)
applied-fill (get applied-tokens :fill)
applied-stroke (get applied-tokens :stroke-color)
fills (:fills shape)
strokes (:strokes shape)
shadows (:shadow shape)
shape-id (:id shape)
fills* (map-indexed
(fn [index fill]
(cond-> (assoc fill :shape-id shape-id :index index)
(and (zero? index) applied-fill)
(assoc :has-token-applied true
:token-name applied-fill)))
fills)
strokes* (map-indexed
(fn [index stroke]
(cond-> (assoc stroke :shape-id shape-id :index index)
(and (zero? index) applied-stroke)
(assoc :has-token-applied true
:token-name applied-stroke)))
strokes)
shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)]
(if (= :text (:type shape))
(-> []
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)
(into (extract-text-colors shape file-id libraries)))
(-> []
(into (keep #(fill->color-att % file-id libraries)) fills*)
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)))))
(defn extract-all-colors
"Extracts color information from a list of shapes, including fills, strokes, and shadows.
If a shape has applied tokens of type :fill or :stroke-color, the first fill or stroke
@ -1366,41 +1403,4 @@
Returns:
A vector of color attribute maps with metadata for each shape."
[shapes file-id libraries]
(reduce
(fn [result shape]
(let [applied-tokens (:applied-tokens shape)
applied-fill (get applied-tokens :fill)
applied-stroke (get applied-tokens :stroke-color)
fills (:fills shape)
strokes (:strokes shape)
shadows (:shadow shape)
shape-id (:id shape)
fills* (map-indexed
(fn [index fill]
(cond-> (assoc fill :shape-id shape-id :index index)
(and (zero? index) applied-fill)
(assoc :has-token-applied true
:token-name applied-fill)))
fills)
strokes* (map-indexed
(fn [index stroke]
(cond-> (assoc stroke :shape-id shape-id :index index)
(and (zero? index) applied-stroke)
(assoc :has-token-applied true
:token-name applied-stroke)))
strokes)
shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)]
(if (= :text (:type shape))
(-> result
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)
(into (extract-text-colors shape file-id libraries)))
(-> result
(into (keep #(fill->color-att % file-id libraries)) fills*)
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
(into (map #(shadow->color-attr % file-id libraries)) shadows*)))))
[]
shapes))
(into [] (mapcat #(shape-colors % file-id libraries)) shapes))

View File

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.library :as ctl]
[app.main.data.workspace.colors :as dwc]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.tokens.application :as dwta]
@ -17,6 +18,17 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(defn- cached-shape-colors
[prev-cache cache libraries-colors shape file-id libraries]
(let [inputs [(:type shape) (:fills shape) (:strokes shape) (:shadow shape)
(:applied-tokens shape) (:content shape) file-id libraries-colors]
[cached-inputs cached] (when prev-cache (.get prev-cache (:id shape)))
colors (if (and cached-inputs (every? true? (map identical? cached-inputs inputs)))
cached
(dwc/shape-colors shape file-id libraries))]
(.set cache (:id shape) [inputs colors])
colors))
(defn- prepare-colors
"Prepares and groups extracted color information from shapes.
Input:
@ -62,8 +74,11 @@
This structure allows fast lookups of all shapes using the same visual color,
regardless of whether it comes from local fills, strokes or shadow-colors."
[shapes file-id libraries]
(let [data (into [] (remove nil?) (dwc/extract-all-colors shapes file-id libraries))
[prev-cache cache libraries-colors shapes file-id libraries]
(let [data (into []
(comp (mapcat #(cached-shape-colors prev-cache cache libraries-colors % file-id libraries))
(remove nil?))
shapes)
groups (d/group-by :attrs #(dissoc % :attrs) data)
;; Unique color attribute maps
@ -98,9 +113,19 @@
(mf/defc color-selection-menu*
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
[{:keys [shapes file-id libraries]}]
(let [{:keys [groups library-colors colors token-colors]}
(let [cache* (mf/use-ref nil)
libraries-colors* (mf/use-ref nil)
{:keys [groups library-colors colors token-colors]}
(mf/with-memo [file-id shapes libraries]
(prepare-colors shapes file-id libraries))
(let [current (mapv (fn [[id library]] [id (ctl/get-colors (:data library))]) libraries)
previous (mf/ref-val libraries-colors*)
libraries-colors (if (= previous current) previous current)
cache (js/Map.)
result (prepare-colors (mf/ref-val cache*) cache libraries-colors shapes file-id libraries)]
(mf/set-ref-val! libraries-colors* libraries-colors)
(mf/set-ref-val! cache* cache)
result))
open* (mf/use-state true)
open? (deref open*)

View File

@ -262,6 +262,20 @@
(assoc :layout-padding (merge-layout-padding values shape-values)))]
(promote-simple-layout-padding-type merged-values)))
(def ^:private content-attrs-cache (js/WeakMap.))
(defn- get-content-attrs
"Moving a text keeps its `:content`, so the attrs read from it are cached by content."
[{:keys [content] :as shape} attrs]
(if (some? content)
(let [[cached-attrs cached] (.get content-attrs-cache content)]
(if (identical? cached-attrs attrs)
cached
(let [result (attrs/get-text-attrs-multi shape txt/default-text-attrs attrs)]
(.set content-attrs-cache content [attrs result])
result)))
(attrs/get-text-attrs-multi shape txt/default-text-attrs attrs)))
(defn get-attrs*
"Given a group of attributes that we want to extract and the shapes to extract them from
returns a list of tuples [id, values] with the extracted properties for the shapes that
@ -359,7 +373,7 @@
(let [shape-attrs (select-keys shape attrs)
content-attrs
(attrs/get-text-attrs-multi shape txt/default-text-attrs attrs)
(get-content-attrs shape attrs)
new-values
(-> values

View File

@ -117,6 +117,7 @@
[frontend-tests.ui.shortcuts-labels-test]
[frontend-tests.ui.sidebar-scroll-test]
[frontend-tests.ui.stroke-menu-test]
[frontend-tests.ui.text-attrs-multiple-test]
[frontend-tests.util-clipboard-test]
[frontend-tests.util-object-test]
[frontend-tests.util-queue-test]
@ -254,6 +255,7 @@
'frontend-tests.ui.shortcuts-labels-test
'frontend-tests.ui.sidebar-scroll-test
'frontend-tests.ui.stroke-menu-test
'frontend-tests.ui.text-attrs-multiple-test
'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test
'frontend-tests.util-queue-test

View File

@ -0,0 +1,40 @@
;; 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 SUBSIDIARY SL
(ns frontend-tests.ui.text-attrs-multiple-test
(:require
[app.main.ui.workspace.sidebar.options.shapes.multiple :as multiple]
[cljs.test :as t :include-macros true]))
(defn- content
[font-family]
{:type "root"
:children [{:type "paragraph-set"
:children [{:type "paragraph"
:children [{:text "a" :font-family font-family}]}]}]})
(def ^:private inter (content "Inter"))
(def ^:private objects
{:board-1 {:id :board-1 :type :frame :shapes [:inner :text-1]}
:inner {:id :inner :type :frame :shapes [:text-2]}
:board-2 {:id :board-2 :type :frame :shapes [:text-3 :rect]}
:text-1 {:id :text-1 :type :text :content inter :applied-tokens {:typography "body"}}
:text-2 {:id :text-2 :type :text :content inter :applied-tokens {:typography "body"}}
:text-3 {:id :text-3 :type :text :content (content "Roboto") :applied-tokens {:typography "body"}}
:rect {:id :rect :type :rect}})
(defn- text-attrs
[labels objects]
(multiple/get-attrs* (map objects labels) objects :text))
(t/deftest boards-read-nested-text-values
(let [[ids values tokens] (text-attrs [:board-1] objects)]
(t/is (= [:text-2 :text-1] ids))
(t/is (= "Inter" (:font-family values)))
(t/is (= "body" (:typography tokens))))
(let [[_ values] (text-attrs [:board-1 :board-2] objects)]
(t/is (= :multiple (:font-family values)))))