♻️ Use interactive update functions only on user actions

This commit is contained in:
Florian Schroedl
2025-08-28 14:21:30 +02:00
committed by Andrés Moya
parent 3cdbc27de9
commit fc5e4a821b
4 changed files with 167 additions and 91 deletions

View File

@@ -10,6 +10,7 @@
[app.common.files.tokens :as cft]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.shape.token :as ctst]
[app.common.types.stroke :as cts]
[app.common.types.text :as txt]
[app.common.types.token :as ctt]
@@ -284,53 +285,71 @@
(when (number? value)
(generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id))))
(defn- generate-font-variant-text-shape-update
"Generate shape update for either updating `:font-family` or `font-weight`.
Try to find the closest weight variant."
(defn warn-font-variant-not-found! []
(st/emit!
(ntf/show {:content (tr "workspace.tokens.font-variant-not-found")
:type :toast
:level :warning
:timeout 7000})))
(defn- update-closest-font-variant-id-by-weight
[txt-attrs target-variant font-id on-mismatch]
(let [font (fonts/get-font-data font-id)
variant (when font
(fonts/find-closest-variant font (:weight target-variant) (:style target-variant)))
call-on-mismatch? (when (and (fn? on-mismatch) variant)
(or
(not= (:font-weight target-variant) (:weight variant))
(when (:font-style target-variant)
(not= (:font-style target-variant) (:style variant)))))]
(when call-on-mismatch?
(on-mismatch))
(cond-> txt-attrs
(:id variant) (assoc :font-variant-id (:id variant)))))
(defn- generate-font-family-text-shape-update
[txt-attrs shape-ids page-id on-mismatch]
(let [update-node? (fn [node]
(let [not-found-font (= (:font-id txt-attrs) (str uuid/zero))
update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(let [font (if (= (:font-id txt-attrs) (str uuid/zero))
(fonts/get-font-data (:font-id node))
(fonts/get-font-data (:font-id txt-attrs)))
variant (when font
(fonts/find-closest-variant font (:weight node) (:style node)))
update-fn (fn [node find-closest-weight?]
(let [font-id (if not-found-font (:font-id node) (:font-id txt-attrs))
txt-attrs (cond-> txt-attrs
(:id variant) (assoc :font-variant-id (:id variant)))
call-on-mismatch? (when (and (fn? on-mismatch) variant)
(or
(not= (:weight txt-attrs) (:weight variant))
(when (:style txt-attrs)
(not= (:style txt-attrs) (:style variant)))))]
(if (or variant (not font))
(do
(when call-on-mismatch? (on-mismatch variant))
(-> node
(d/txt-merge txt-attrs)
(cty/remove-typography-from-node)))
node)))]
find-closest-weight? (update-closest-font-variant-id-by-weight node font-id on-mismatch))]
(-> node
(d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
(fn [shape]
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
{:ignore-touched true
:page-id page-id})))
(defn- create-font-family-text-attrs
[value]
(let [font-family (-> (first value)
;; Strip quotes around font-family like `"Inter"`
(str/trim #"[\"']"))
font (some-> font-family
(fonts/find-font-family))]
(if font
{:font-id (:id font)
:font-family (:family font)}
{:font-id (str uuid/zero)
:font-family font-family})))
(defn update-font-family
([value shape-ids attributes] (update-font-family value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(let [font-family (-> (first value)
;; Strip quotes around font-family like `"Inter"`
(str/trim #"[\"']"))
font-family (some-> font-family
(fonts/find-font-family))
text-attrs (if font-family
{:font-id (:id font-family)
:font-family (:family font-family)}
{:font-id (str uuid/zero)
:font-family font-family})]
(when text-attrs
(generate-font-variant-text-shape-update text-attrs shape-ids page-id nil)))))
(when-let [text-attrs (create-font-family-text-attrs value)]
(generate-font-family-text-shape-update text-attrs shape-ids page-id nil))))
(defn update-font-family-interactive
([value shape-ids attributes] (update-font-family-interactive value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(when-let [text-attrs (create-font-family-text-attrs value)]
(generate-font-family-text-shape-update text-attrs shape-ids page-id warn-font-variant-not-found!))))
(defn update-font-size
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
@@ -360,22 +379,35 @@
(st/emit! (ptk/data-event :expand-text-more-options))
(update-text-decoration value shape-ids attributes page-id))))
(defn- generate-font-weight-text-shape-update
[font-variant shape-ids page-id on-mismatch]
(let [font-variant (assoc font-variant
:font-weight (:weight font-variant)
:font-style (:style font-variant))
update-node? (fn [node]
(or (txt/is-text-node? node)
(txt/is-paragraph-node? node)))
update-fn (fn [node _]
(let [txt-attrs (update-closest-font-variant-id-by-weight font-variant font-variant (:font-id node) on-mismatch)]
(-> node
(d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id})))
(defn update-font-weight
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(when-let [font-variant (ctt/valid-font-weight-variant value)]
(generate-font-variant-text-shape-update font-variant shape-ids page-id nil))))
(generate-font-weight-text-shape-update font-variant shape-ids page-id nil))))
(defn update-font-weight-interactive
([value shape-ids attributes] (update-font-weight-interactive value shape-ids attributes nil))
([value shape-ids _attributes page-id]
(when-let [font-variant (ctt/valid-font-weight-variant value)]
(let [on-mismatch #(st/emit! (ntf/show {:content (tr "workspace.tokens.font-variant-not-found")
:type :toast
:level :warning
:timeout 7000}))]
(generate-font-variant-text-shape-update font-variant shape-ids page-id on-mismatch)))))
(generate-font-weight-text-shape-update font-variant shape-ids page-id warn-font-variant-not-found!))))
(defn- apply-functions-map
"Apply map of functions `fs` to a map of values `vs` using `args`.
@@ -404,6 +436,21 @@
value
[shape-ids attributes page-id])))))
(defn update-typography-interactive
([value shape-ids attributes] (update-typography value shape-ids attributes nil))
([value shape-ids attributes page-id]
(when (map? value)
(rx/merge
(apply-functions-map
{:font-size update-font-size
:font-family update-font-family-interactive
:font-weight update-font-weight-interactive
:letter-spacing update-letter-spacing
:text-case update-text-case
:text-decoration update-text-decoration-interactive}
value
[shape-ids attributes page-id])))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
(defn apply-token
@@ -579,7 +626,7 @@
:font-family
{:title "Font Family"
:attributes ctt/font-family-keys
:on-update-shape update-font-family
:on-update-shape update-font-family-interactive
:modal {:key :tokens/font-family
:fields [{:label "Font Family"
:key :font-family}]}}

View File

@@ -22,38 +22,6 @@
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; Constants -------------------------------------------------------------------
(def ^:private filter-existing-values? false)
(def ^:private attributes->shape-update
{ctt/border-radius-keys dwta/update-shape-radius-for-corners
ctt/color-keys dwta/update-fill-stroke
ctt/stroke-width-keys dwta/update-stroke-width
ctt/sizing-keys dwta/update-shape-dimensions
ctt/opacity-keys dwta/update-opacity
#{:line-height} dwta/update-line-height
#{:font-size} dwta/update-font-size
#{:letter-spacing} dwta/update-letter-spacing
#{:font-family} dwta/update-font-family
#{:text-case} dwta/update-text-case
#{:text-decoration} dwta/update-text-decoration
#{:font-weight} dwta/update-font-weight
#{:typography} dwta/update-typography
#{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
#{:column-gap :row-gap} dwta/update-layout-spacing
#{:width :height} dwta/update-shape-dimensions
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits
ctt/rotation-keys dwta/update-rotation})
(def attribute-actions-map
(reduce
(fn [acc [ks action]]
(into acc (map (fn [k] [k action]) ks)))
{} attributes->shape-update))
;; Helpers ---------------------------------------------------------------------
;; TODO: see if this can be replaced by more standard functions
@@ -67,6 +35,56 @@
([a b & rest]
(reduce deep-merge a (cons b rest))))
(defn- flatten-set-keyed-map
"Flattens a map where the keys are sets of keywords."
[m into-m]
(reduce
(fn [acc [ks action]]
(into acc (map (fn [k] [k action]) ks)))
into-m m))
;; Constants -------------------------------------------------------------------
(def ^:private filter-existing-values? false)
(def ^:private attributes->shape-update
{ctt/border-radius-keys dwta/update-shape-radius-for-corners
ctt/color-keys dwta/update-fill-stroke
ctt/stroke-width-keys dwta/update-stroke-width
ctt/sizing-keys dwta/update-shape-dimensions
ctt/opacity-keys dwta/update-opacity
ctt/rotation-keys dwta/update-rotation
;; Typography
ctt/font-family-keys dwta/update-font-family
ctt/font-size-keys dwta/update-font-size
ctt/font-weight-keys dwta/update-font-weight
ctt/letter-spacing-keys dwta/update-letter-spacing
ctt/text-case-keys dwta/update-text-case
ctt/text-decoration-keys dwta/update-text-decoration
ctt/typography-token-keys dwta/update-typography
#{:line-height} dwta/update-line-height
;; Layout
#{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
#{:column-gap :row-gap} dwta/update-layout-spacing
#{:width :height} dwta/update-shape-dimensions
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits})
(def ^:private attribute-actions-map
(flatten-set-keyed-map attributes->shape-update {}))
(def ^:private interactive-attributes->shape-update
{ctt/font-family-keys dwta/update-font-family-interactive
ctt/font-weight-keys dwta/update-font-weight-interactive
ctt/text-decoration-keys dwta/update-text-decoration-interactive
ctt/typography-token-keys dwta/update-typography-interactive})
(def ^:private interactive-attribute-actions-map
(flatten-set-keyed-map interactive-attributes->shape-update attribute-actions-map))
;; Data flows ------------------------------------------------------------------
(defn- invert-collect-key-vals
@@ -130,19 +148,26 @@
[tokens frame-ids text-ids])))
(defn- actionize-shapes-update-info [page-id shapes-update-info]
(mapcat (fn [[attrs update-infos]]
(let [action (some attribute-actions-map attrs)]
(assert (fn? action) "missing action function on attributes->shape-update")
(map
(fn [[v shape-ids]]
(action v shape-ids attrs page-id))
update-infos)))
shapes-update-info))
(defn- actionize-shapes-update-info
[page-id shapes-update-info interactive?]
(let [attribute-actions (if interactive?
interactive-attribute-actions-map
attribute-actions-map)]
(mapcat (fn [[attrs update-infos]]
(let [action (some attribute-actions attrs)]
(assert (fn? action) "missing action function on attributes->shape-update")
(map
(fn [[v shape-ids]]
(action v shape-ids attrs page-id))
update-infos)))
shapes-update-info)))
(defn propagate-tokens
"Propagate tokens values to all shapes where they are applied"
[state resolved-tokens]
"Propagate tokens values to all shapes where they are applied
Pass `interactive?` to indicate the propagation was triggered by a user interaction
and should use update functions that may execute ui side-effects like showing warnings."
[state resolved-tokens interactive?]
(let [file-id (get state :current-file-id)
current-page-id (get state :current-page-id)
fdata (dsh/lookup-file-data state file-id)
@@ -162,7 +187,7 @@
(collect-shapes-update-info resolved-tokens (:objects page))
actions
(actionize-shapes-update-info page-id attrs)
(actionize-shapes-update-info page-id attrs interactive?)
;; Composed updates return observables and need to be executed differently
{:keys [observable normal]} (group-by #(if (rx/observable? %) :observable :normal) actions)]
@@ -193,7 +218,11 @@
(l/inf :status "END" :hint "propagate-tokens" :elapsed elapsed)))))))
(defn propagate-workspace-tokens
[]
"Updates styles for tokens.
Pass `interactive?` to indicate the propagation was triggered by a user interaction
and should use update functions that may execute ui side-effects like showing warnings."
[& {:keys [interactive?]}]
(ptk/reify ::propagate-workspace-tokens
ptk/WatchEvent
(watch [_ state _]
@@ -205,5 +234,5 @@
(let [undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id :timeout false))
(propagate-tokens state sd-tokens)
(propagate-tokens state sd-tokens interactive?)
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))

View File

@@ -469,7 +469,7 @@ custom-input-token-value-props: Custom props passed to the custom-input-token-va
{:name final-name
:value (:value valid-token)
:description final-description}))
(dwtp/propagate-workspace-tokens)
(dwtp/propagate-workspace-tokens :interactive? true)
(modal/hide)))))))))
on-delete-token

View File

@@ -908,7 +908,7 @@
(t/is (= (:typography (:applied-tokens text-1')) "typography.heading"))
(t/is (= (:font-size style-text-blocks) "24"))
(t/is (= (:font-weight style-text-blocks) "400"))
(t/is (= (:font-weight style-text-blocks) "700"))
(t/is (= (:font-family style-text-blocks) "sourcesanspro"))
(t/is (= (:letter-spacing style-text-blocks) "2"))
(t/is (= (:text-transform style-text-blocks) "uppercase"))