0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-14 07:51:35 -05:00

🚧 Tokens (part4) (#5952)

*  Use reduce-kv for creating tokens tree

Instead of reduce

*  Avoid double iteration on spliting text shapes from shapes

On processing color changes

* ♻️ Move the undo transaction out of transform-fill

The undo transaction is a high level construct and it should
be called from the first level events. Low level impl should
not handle transactions at all

*  Use low-level primitives for update-fill token event

* 📎 Add performance logging for style dictionary resolution

* 📎 Replace inline code with a helper

* ♻️ Refactor how fill and stroke color is applied on tokens

* 💄 Fix call convention for component-item-thumbnail component

*  Clean component thumbnail when frame change received

* 📎 Fix tests

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
This commit is contained in:
Andrey Antukh 2025-02-28 09:47:13 +01:00 committed by GitHub
parent 65c6c821e7
commit c9a8d2bd23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 236 additions and 162 deletions

View file

@ -200,7 +200,7 @@
result))))
(defn get-parent-seq
"Returns a vector of parents of the specified shape."
"Returns a lazy seq of parents of the specified shape."
([objects shape-id]
(get-parent-seq objects (get objects shape-id) shape-id))

View file

@ -118,10 +118,10 @@
(def valid-color?
(sm/lazy-validator schema:color))
(def check-color!
(def check-color
(sm/check-fn schema:color :hint "expected valid color struct"))
(def check-recent-color!
(def check-recent-color
(sm/check-fn schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -148,7 +148,7 @@
(sm/register! ::stroke schema:stroke)
(def check-stroke!
(def check-stroke
(sm/check-fn schema:stroke))
(def schema:shape-base-attrs

View file

@ -28,5 +28,5 @@
(sm/register! ::shadow schema:shadow)
(def check-shadow!
(def check-shadow
(sm/check-fn schema:shadow))

View file

@ -286,11 +286,10 @@
Optionally use `update-token-fn` option to transform the token."
[tokens & {:keys [update-token-fn]
:or {update-token-fn identity}}]
(reduce
(fn [acc [_ token]]
(let [path (split-token-path (:name token))]
(assoc-in acc path (update-token-fn token))))
{} tokens))
(reduce-kv (fn [acc _ token]
(let [path (split-token-path (:name token))]
(assoc-in acc path (update-token-fn token))))
{} tokens))
(defn backtrace-tokens-tree
"Convert tokens into a nested tree with their `:name` as the path.

View file

@ -13,8 +13,8 @@
[app.common.schema :as sm]
[app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.shape :refer [check-stroke!]]
[app.common.types.shape.shadow :refer [check-shadow!]]
[app.common.types.shape :refer [check-stroke]]
[app.common.types.shape.shadow :refer [check-shadow]]
[app.main.broadcast :as mbc]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
@ -81,19 +81,45 @@
(assoc-in [:workspace-global :picked-color-select] value)
(assoc-in [:workspace-global :picked-shift?] shift?)))))
(defn- split-text-shapes
"Split text shapes from non-text shapes"
[objects ids]
(loop [ids (seq ids)
text-ids []
shape-ids []]
(if-let [id (first ids)]
(let [shape (get objects id)]
(if (cfh/text-shape? shape)
(recur (rest ids)
(conj text-ids id)
shape-ids)
(recur (rest ids)
text-ids
(conj shape-ids id))))
[text-ids shape-ids])))
(defn assoc-shape-fill
[shape position fill]
(update shape :fills
(fn [fills]
(if (nil? fills)
[fill]
(assoc fills position fill)))))
(defn transform-fill
([state ids color transform] (transform-fill state ids color transform nil))
"A low level function that creates a shape fill transformations stream"
([state ids color transform]
(transform-fill state ids color transform nil))
([state ids color transform options]
(let [page-id (or (get options :page-id)
(get state :current-page-id))
objects (dsh/lookup-page-objects state page-id)
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
shape-ids (remove is-text? ids)
undo-id (js/Symbol)
[text-ids shape-ids]
(split-text-shapes objects ids)
attrs
fill
(cond-> {}
(contains? color :color)
(assoc :fill-color (:color color))
@ -116,13 +142,12 @@
:always
(d/without-nils))
transform-attrs #(transform % attrs)]
transform-attrs #(transform % fill)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(rx/from (map #(dwt/update-text-with-function % transform-attrs options) text-ids))
(rx/of (dwsh/update-shapes shape-ids transform-attrs options))
(rx/of (dwu/commit-undo-transaction undo-id))))))
(->> (rx/from text-ids)
(rx/map #(dwt/update-text-with-function % transform-attrs options)))
(rx/of (dwsh/update-shapes shape-ids transform-attrs options))))))
(defn swap-attrs [shape attr index new-index]
(let [first (get-in shape [attr index])
@ -154,12 +179,12 @@
(ptk/reify ::change-fill
ptk/WatchEvent
(watch [_ state _]
(let [change-fn (fn [shape attrs]
(-> shape
(cond-> (not (contains? shape :fills))
(assoc :fills []))
(assoc-in [:fills position] (into {} attrs))))]
(transform-fill state ids color change-fn options))))))
(let [change-fn #(assoc-shape-fill %1 position %2)
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(transform-fill state ids color change-fn options)
(rx/of (dwu/commit-undo-transaction undo-id))))))))
(defn change-fill-and-clear
([ids color] (change-fill-and-clear ids color nil))
@ -167,68 +192,84 @@
(ptk/reify ::change-fill-and-clear
ptk/WatchEvent
(watch [_ state _]
(let [set (fn [shape attrs] (assoc shape :fills [attrs]))]
(transform-fill state ids color set options))))))
(let [change-fn (fn [shape attrs] (assoc shape :fills [attrs]))
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id))
(transform-fill state ids color change-fn options)
(rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn add-fill
([ids color] (add-fill ids color nil))
([ids color options]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(assert
(ctc/check-color color)
"expected a valid color struct")
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(assert
(every? uuid? ids)
"expected a valid coll of uuid's")
(ptk/reify ::add-fill
ptk/WatchEvent
(watch [_ state _]
(let [add (fn [shape attrs]
(-> shape
(update :fills #(into [attrs] %))))]
(transform-fill state ids color add options))))))
(let [change-fn
(fn [shape attrs]
(-> shape
(update :fills #(into [attrs] %))))
undo-id
(js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id))
(transform-fill state ids color change-fn options)
(rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn remove-fill
([ids color position] (remove-fill ids color position nil))
([ids color position options]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(assert (ctc/check-color color)
"expected a valid color struct")
(assert (every? uuid? ids)
"expected a valid coll of uuid's")
(ptk/reify ::remove-fill
ptk/WatchEvent
(watch [_ state _]
(let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
(filterv (fn [[idx _]] (not= idx index)))
(mapv second)))
(let [remove-fill-by-index
(fn [values index]
(->> (d/enumerate values)
(filterv (fn [[idx _]] (not= idx index)))
(mapv second)))
remove (fn [shape _] (update shape :fills remove-fill-by-index position))]
(transform-fill state ids color remove options))))))
change-fn
(fn [shape _] (update shape :fills remove-fill-by-index position))
undo-id
(js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(transform-fill state ids color change-fn options)
(rx/of (dwu/commit-undo-transaction undo-id))))))))
(defn remove-all-fills
([ids color] (remove-all-fills ids color nil))
([ids color options]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(assert (ctc/check-color color) "expected a valid color struct")
(assert (every? uuid? ids) "expected a valid coll of uuid's")
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-all-fills
ptk/WatchEvent
(watch [_ state _]
(let [remove-all (fn [shape _] (assoc shape :fills []))]
(transform-fill state ids color remove-all options))))))
(let [change-fn (fn [shape _] (assoc shape :fills []))
undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(transform-fill state ids color change-fn options)
(rx/of (dwu/commit-undo-transaction undo-id))))))))
(defn change-hide-fill-on-export
[ids hide-fill-on-export]
@ -252,6 +293,8 @@
:stroke-cap-start
:stroke-cap-end])
;; FIXME: this function initializes an empty stroke, maybe we can move
;; it to common.types
(defn- build-stroke-style-attrs
[stroke]
(let [attrs (select-keys stroke stroke-style-attrs)]
@ -268,6 +311,40 @@
:always
(d/without-nils))))
(defn update-shape-stroke-color
"Given a shape, update color attributes on the stroke on the specified
`position`; if no stroke is found a new empty stroke is created."
[shape position color]
(update shape :strokes
(fn [strokes]
(let [stroke (if (nil? strokes)
(build-stroke-style-attrs nil)
(build-stroke-style-attrs (get strokes position)))
stroke (cond-> (build-stroke-style-attrs stroke)
(contains? color :color)
(assoc :stroke-color (:color color))
(contains? color :id)
(assoc :stroke-color-ref-id (:id color))
(contains? color :file-id)
(assoc :stroke-color-ref-file (:file-id color))
(contains? color :gradient)
(assoc :stroke-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :stroke-opacity (:opacity color))
(contains? color :image)
(assoc :stroke-image (:image color))
:always
(d/without-nils))]
(if (nil? strokes)
[stroke]
(assoc strokes position stroke))))))
(defn change-stroke-color
([ids color index] (change-stroke-color ids color index nil))
([ids color index options]
@ -275,38 +352,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (let [options (assoc options :changed-sub-attr [:stroke-color])]
(dwsh/update-shapes
ids
(fn [shape]
(let [stroke (get-in shape [:strokes index])
attrs (cond-> (build-stroke-style-attrs stroke)
(contains? color :color)
(assoc :stroke-color (:color color))
(contains? color :id)
(assoc :stroke-color-ref-id (:id color))
(contains? color :file-id)
(assoc :stroke-color-ref-file (:file-id color))
(contains? color :gradient)
(assoc :stroke-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :stroke-opacity (:opacity color))
(contains? color :image)
(assoc :stroke-image (:image color))
:always
(d/without-nils))]
(cond-> shape
(not (contains? shape :strokes))
(assoc :strokes [])
:always
(assoc-in [:strokes index] attrs))))
options)))))))
(dwsh/update-shapes ids #(update-shape-stroke-color % index color) options)))))))
(defn change-stroke-attrs
([ids attrs index] (change-stroke-attrs ids attrs index nil))
@ -354,13 +400,13 @@
(defn add-shadow
[ids shadow]
(dm/assert!
"expected a valid shadow struct"
(check-shadow! shadow))
(assert
(check-shadow shadow)
"expected a valid shadow struct")
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(assert
(every? uuid? ids)
"expected a valid coll of uuid's")
(ptk/reify ::add-shadow
ptk/WatchEvent
@ -372,13 +418,14 @@
(defn add-stroke
[ids stroke]
(dm/assert!
"expected a valid stroke struct"
(check-stroke! stroke))
(assert
(check-stroke stroke)
"expected a valid stroke struct")
(assert
(every? uuid? ids)
"expected a valid coll of uuid's")
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::add-stroke
ptk/WatchEvent
@ -391,9 +438,9 @@
(defn remove-stroke
[ids position]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(assert
(every? uuid? ids)
"expected a valid coll of uuid's")
(ptk/reify ::remove-stroke
ptk/WatchEvent
@ -411,9 +458,10 @@
(defn remove-all-strokes
[ids]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(assert
(every? uuid? ids)
"expected a valid coll of uuid's")
(ptk/reify ::remove-all-strokes
ptk/WatchEvent
@ -508,23 +556,22 @@
(def ^:private schema:change-color-operations
[:vector schema:change-color-operation])
(def ^:private check-change-color-operations!
(def ^:private check-change-color-operations
(sm/check-fn schema:change-color-operations))
(defn change-color-in-selected
[operations new-color old-color]
(dm/assert!
"expected valid color operations"
(check-change-color-operations! operations))
(assert (check-change-color-operations operations)
"expected valid color operations")
(dm/assert!
"expected valid color structure"
(ctc/check-color! new-color))
(assert
(ctc/check-color new-color)
"expected valid color structure")
(dm/assert!
"expected valid color structure"
(ctc/check-color! old-color))
(assert
(ctc/check-color old-color)
"expected valid color structure")
(ptk/reify ::change-color-in-selected
ptk/WatchEvent
@ -546,9 +593,9 @@
(defn apply-color-from-palette
[color stroke?]
(dm/assert!
"expected valid color structure"
(ctc/check-color! color))
(assert
(ctc/check-color color)
"expected valid color structure")
(ptk/reify ::apply-color-from-palette
ptk/WatchEvent
@ -585,9 +632,8 @@
(defn apply-color-from-colorpicker
[color]
(dm/assert!
"expected valid color structure"
(ctc/check-color! color))
(assert (ctc/check-color color)
"expected valid color structure")
(ptk/reify ::apply-color-from-colorpicker
ptk/UpdateEvent

View file

@ -123,7 +123,7 @@
(dm/assert!
"expect valid color structure"
(ctc/check-color! color))
(ctc/check-color color))
(ptk/reify ::add-color
ev/Event
@ -140,10 +140,8 @@
(defn add-recent-color
[color]
(dm/assert!
"expected valid recent color structure"
(ctc/check-recent-color! color))
(assert (ctc/check-recent-color color)
"expected valid recent color structure")
(ptk/reify ::add-recent-color
ptk/UpdateEvent
@ -182,7 +180,7 @@
(dm/assert!
"expected valid color data structure"
(ctc/check-color! color))
(ctc/check-color color))
(dm/assert!
"expected file-id"
@ -200,7 +198,7 @@
(dm/assert!
"expected valid color data structure"
(ctc/check-color! color))
(ctc/check-color color))
(dm/assert!
"expected file-id"

View file

@ -282,8 +282,9 @@
;; and interrupt any ongoing update-thumbnail process
;; related to current frame-id
(->> all-commits-s
(rx/map (fn [frame-id]
(clear-thumbnail file-id page-id frame-id "frame"))))
(rx/mapcat (fn [frame-id]
(rx/of (clear-thumbnail file-id page-id frame-id "frame")
(clear-thumbnail file-id page-id frame-id "component")))))
;; Generate thumbnails in batches, once user becomes
;; inactive for some instant.

View file

@ -282,9 +282,8 @@
(:id target-asset)
(cfh/merge-path-item prefix (:name target-asset))))))))
(mf/defc component-item-thumbnail
(mf/defc component-item-thumbnail*
"Component that renders the thumbnail image or the original SVG."
{::mf/props :obj}
[{:keys [file-id root-shape component container class is-hidden]}]
(let [page-id (:main-instance-page component)
root-id (:main-instance-id component)

View file

@ -158,13 +158,14 @@
(when ^boolean dragging?
[:div {:class (stl/css :dragging)}])]
[:& cmm/component-item-thumbnail {:file-id file-id
:class (stl/css-case :thumbnail true
:asset-list-thumbnail (not listing-thumbs?))
:root-shape root-shape
:component component
:container container
:is-hidden (not visible?)}]])]))
[:> cmm/component-item-thumbnail*
{:file-id file-id
:class (stl/css-case :thumbnail true
:asset-list-thumbnail (not listing-thumbs?))
:root-shape root-shape
:component component
:container container
:is-hidden (not visible?)}]])]))
(mf/defc components-group
{::mf/wrap-props false}

View file

@ -322,10 +322,11 @@
:key (str "swap-item-" (:id item))
:on-click on-select}
(when visible?
[:& cmm/component-item-thumbnail {:file-id (:file-id item)
:root-shape root-shape
:component item
:container container}])
[:> cmm/component-item-thumbnail*
{:file-id (:file-id item)
:root-shape root-shape
:component item
:container container}])
[:span {:class (stl/css-case :component-name true
:selected (= (:id item) component-id))}
(if is-search (:full-name item) (:name item))]]))

View file

@ -170,18 +170,42 @@
(f shape-ids {:color hex :opacity opacity} 0 {:ignore-touched true
:page-id page-id}))))
(defn- value->color
"Transform a token color value into penpot color data structure"
[color]
(when-let [tc (tinycolor/valid-color color)]
(let [hex (tinycolor/->hex-string tc)
opacity (tinycolor/alpha tc)]
{:color hex :opacity opacity})))
(defn update-fill
([value shape-ids attributes] (update-fill value shape-ids attributes nil))
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
(update-color wdc/change-fill value shape-ids page-id)))
([value shape-ids attributes]
(update-fill value shape-ids attributes nil))
([value shape-ids _attributes page-id] ;; The attributes param is needed to have the same arity that other update functions
(ptk/reify ::update-fill
ptk/WatchEvent
(watch [_ state _]
(when-let [color (value->color value)]
(let [update-fn #(wdc/assoc-shape-fill %1 0 %2)]
(wdc/transform-fill state shape-ids color update-fn {:ignore-touched true
:page-id page-id})))))))
(defn update-stroke-color
([value shape-ids attributes] (update-stroke-color value shape-ids attributes nil))
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
(update-color wdc/change-stroke-color value shape-ids page-id)))
([value shape-ids attributes]
(update-stroke-color value shape-ids attributes nil))
;; The attributes param is needed to have the same arity that other update functions
([value shape-ids _attributes page-id]
(when-let [color (value->color value)]
(dwsh/update-shapes shape-ids
#(wdc/update-shape-stroke-color % 0 color)
{:page-id page-id
:ignore-touched true
:changed-sub-attr [:stroke-color]}))))
(defn update-fill-stroke
([value shape-ids attributes] (update-fill-stroke value shape-ids attributes nil))
([value shape-ids attributes]
(update-fill-stroke value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::update-fill-stroke
ptk/WatchEvent

View file

@ -10,12 +10,13 @@
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.warnings :as wtw]
[app.util.i18n :refer [tr]]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]))
(l/set-level! "app.main.ui.workspace.tokens.style-dictionary" :warn)
(l/set-level! :debug)
;; === Style Dictionary
@ -158,9 +159,10 @@
config)
(build-dictionary [_]
(-> (sd. (clj->js config))
(.buildAllPlatforms "json")
(p/then #(.-allTokens ^js %)))))
(let [config' (clj->js config)]
(-> (sd. config')
(.buildAllPlatforms "json")
(p/then #(.-allTokens ^js %))))))
(defn resolve-tokens-tree+
([tokens-tree get-token]
@ -302,11 +304,14 @@
(let [state* (mf/use-state tokens)]
(mf/with-effect [tokens interactive?]
(when (seq tokens)
(let [promise (if interactive?
(let [tpoint (dt/tpoint-ms)
promise (if interactive?
(resolve-tokens-interactive+ tokens)
(resolve-tokens+ tokens))]
(->> promise
(p/fmap (fn [resolved-tokens]
(reset! state* resolved-tokens)))))))
(let [elapsed (tpoint)]
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
(reset! state* resolved-tokens))))))))
@state*))