mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
✨ Give a synced copy text variants of its own font families
Sync and the variant switch can take a copy text's family from one side and its weight from the other. The variant id goes with the weight, so it can name a variant of the other family: the WASM renderer then picks the closest variant, but the SVG renderer falls back to the regular one. The frontend now gives each text content that a sync or a component swap commits variants of its own families: a run whose variant is not one of its family's takes the family's closest to its weight and style (cttx/update-font-variants, fonts/find-closest-variant). The pure runner has no fonts, so case BA leaves variant ids out of its comparison. The merge also leaves alone a run that already shows the placed style, so a main edit no longer rewrites the variant ids of a copy whose family is its own; with a new family, the run still takes the placed variant id. Benchmark (lower quantile, ms; baseline / now / ratio): deepest-edit-through-three-levels 35.2 / 35.7 / 1.01 edit-through-a-chain-of-dependents 68.9 / 65.2 / 0.95 library-update-into-copies 83.1 / 75.1 / 0.90 main-edit-into-10-copies 1.8 / 1.5 / 0.86 main-edit-into-100-copies 15.4 / 15.1 / 0.98 main-edit-into-1000-copies 153.1 / 151.4 / 0.99 variant-switch-into-copies 32.1 / 30.4 / 0.95 AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
5586625233
commit
1dcae42878
@ -86,7 +86,8 @@
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
[clojure.test :as t]
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
(def ^:private red "#ff0000")
|
||||
(def ^:private green "#00ff00")
|
||||
@ -705,6 +706,15 @@
|
||||
thtx/colors))
|
||||
:font-family #(thtx/restyle % :family value))))
|
||||
|
||||
(defn- without-variants
|
||||
"`text` in the notation with no `:font-variant-id`."
|
||||
[text]
|
||||
(walk/postwalk #(cond-> % (map? %) (dissoc :font-variant-id)) text))
|
||||
|
||||
(defn- normalize
|
||||
[text]
|
||||
(without-variants (thtx/normalize text)))
|
||||
|
||||
(defn text-style-override-against-main-style-change
|
||||
"Case BA. A main text in two styles (\"Price: \" and a bold \"$10\") and
|
||||
its copy. The copy changes the fill or the font family of all of its text,
|
||||
@ -726,34 +736,32 @@
|
||||
:asserter (fn [s]
|
||||
(let [copy-node (tm/get-choice s copy-edit)
|
||||
main-node (tm/get-choice s main-edit)
|
||||
same? (= (:edit copy-node) (:edit main-node))
|
||||
main-text ((text-restyle main-node) setup/two-style-text)
|
||||
merged (cond-> ((text-restyle copy-node) main-text)
|
||||
;; the weight, from the main, keeps the
|
||||
;; bold's variant id, for the frontend to
|
||||
;; pick the copy's family's
|
||||
(and (= :font-family (:edit copy-node)) (not same?))
|
||||
(update-in [0 1 1] assoc :font-variant-id "bold"))
|
||||
merged ((text-restyle copy-node) main-text)
|
||||
copy (setup/copy-instance s)
|
||||
main (setup/main-instance s)
|
||||
text-of #(n/text-span-value % :content)]
|
||||
;; the variant id is left out: with the family from
|
||||
;; one side and the weight from the other, the
|
||||
;; frontend picks the family's closest variant, and
|
||||
;; the pure runner has no fonts to pick from
|
||||
text-of #(without-variants (n/text-span-value % :content))]
|
||||
(cond
|
||||
(tm/applied? s reset)
|
||||
(do
|
||||
(t/is (= (thtx/normalize main-text) (text-of copy)))
|
||||
(t/is (= (thtx/normalize main-text) (text-of main)))
|
||||
(t/is (= (normalize main-text) (text-of copy)))
|
||||
(t/is (= (normalize main-text) (text-of main)))
|
||||
(t/is (empty? (:touched copy))))
|
||||
|
||||
(tm/applied? s push)
|
||||
(do
|
||||
(t/is (= (thtx/normalize merged) (text-of main)))
|
||||
(t/is (= (normalize merged) (text-of main)))
|
||||
(t/is (= (text-of main) (text-of copy)))
|
||||
(t/is (empty? (:touched copy))))
|
||||
|
||||
:else
|
||||
(do
|
||||
(t/is (= (thtx/normalize main-text) (text-of main)))
|
||||
(t/is (= (thtx/normalize merged) (text-of copy)))))))}))
|
||||
(t/is (= (normalize main-text) (text-of main)))
|
||||
(t/is (= (normalize merged) (text-of copy)))))))}))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Swap outcomes
|
||||
|
||||
@ -913,10 +913,17 @@
|
||||
and on a paragraph when it has one or no run of it has."
|
||||
[dst placed drop-typo?]
|
||||
(let [run-units (filterv #(= :run (get-in style-units [% :level])) (keys placed))
|
||||
;; a run that already shows the placed style keeps its own values;
|
||||
;; but its variant id names a font of its own family, so with a new
|
||||
;; family it takes the placed one
|
||||
family? (contains? placed :text-content-family)
|
||||
write-run (fn [node styles]
|
||||
(cond-> (reduce (fn [node [unit style]]
|
||||
(write-style node style (get-in style-units [unit :attrs])
|
||||
(= unit :text-content-line-height)))
|
||||
(let [attrs (get-in style-units [unit :attrs])]
|
||||
(if (and (= (style-key unit (select-keys node attrs)) (style-key unit style))
|
||||
(not (and family? (= unit :text-content-weight))))
|
||||
node
|
||||
(write-style node style attrs (= unit :text-content-line-height)))))
|
||||
node
|
||||
(map vector run-units styles))
|
||||
drop-typo? drop-typography))
|
||||
@ -1101,6 +1108,20 @@
|
||||
(not same-letters?)
|
||||
(conj :text-content-text))))
|
||||
|
||||
(defn update-font-variants
|
||||
"`content` with the variant id of each node that names a font set to
|
||||
`(variant-of node)`, when that gives one. Sync and the variant switch
|
||||
can take a text's family from one side and its weight from the other,
|
||||
leaving a variant id of another family; the frontend, which knows the
|
||||
fonts, picks the family's closest variant through this."
|
||||
[content variant-of]
|
||||
(transform-nodes #(and (is-content-node? %) (some? (:font-id %)))
|
||||
(fn [node]
|
||||
(if-some [variant-id (variant-of node)]
|
||||
(assoc node :font-variant-id variant-id)
|
||||
node))
|
||||
content))
|
||||
|
||||
(defn content-has-text?
|
||||
[content search]
|
||||
(let [search-lower (str/lower search)]
|
||||
|
||||
@ -107,3 +107,10 @@
|
||||
(t/is (not (cttx/same-style? {:font-size "14"} {:font-size "18"})))
|
||||
(t/is (not (cttx/same-style? {:font-variant-id "700"} {:font-variant-id "700italic"})))
|
||||
(t/is (not (cttx/same-style? {:fills [{:fill-color "#000000" :fill-opacity 1}]} {}))))
|
||||
|
||||
(t/deftest font-variants-of-the-nodes-that-name-a-font
|
||||
(let [content (thtx/content [[["Price: "] ["$10" {:weight 700}]]])
|
||||
fixed (cttx/update-font-variants content #(when (= "bold" (:font-variant-id %)) "700"))]
|
||||
(t/is (= [[["Price: "] ["$10" {:weight 700 :font-variant-id "700"}]]] (thtx/view fixed)))
|
||||
(t/testing "the paragraph, which holds its first run's styles, keeps them"
|
||||
(t/is (= "regular" (get-in fixed [:children 0 :children 0 :font-variant-id]))))))
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.library :as ctl]
|
||||
[app.common.types.text :as cttx]
|
||||
[app.common.types.typography :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@ -53,6 +54,7 @@
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.features :as features]
|
||||
[app.main.features.pointer-map :as fpmap]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
@ -981,6 +983,32 @@
|
||||
(rx/of (dwt.wasm/render-thumbnail file-id page-id root-id :persist? true)))
|
||||
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))))
|
||||
|
||||
(defn- closest-font-variant
|
||||
"The id of the variant of the family of the text node `node` closest to its
|
||||
weight and style, when its variant is not one of the family's."
|
||||
[node]
|
||||
(when-let [font (fonts/get-font-data (:font-id node))]
|
||||
(when-not (d/seek #(= (:id %) (:font-variant-id node)) (:variants font))
|
||||
(:id (fonts/find-closest-variant font (:font-weight node) (:font-style node))))))
|
||||
|
||||
(defn fix-font-variants
|
||||
"`changes` with each text content they set given variants of its own
|
||||
families: a sync or a variant switch can take a copy text's family from
|
||||
one side and its weight from the other (`mem:common/component-sync-contract`)."
|
||||
[changes]
|
||||
(let [fix-operation
|
||||
(fn [{:keys [type attr val] :as operation}]
|
||||
(if (and (= type :set) (= attr :content) (cttx/is-root-node? val))
|
||||
(update operation :val cttx/update-font-variants closest-font-variant)
|
||||
operation))]
|
||||
(update changes :redo-changes
|
||||
(fn [redo-changes]
|
||||
(mapv (fn [change]
|
||||
(cond-> change
|
||||
(= :mod-obj (:type change))
|
||||
(update :operations #(mapv fix-operation %))))
|
||||
redo-changes)))))
|
||||
|
||||
(defn component-swap
|
||||
"Swaps a component with another one"
|
||||
[shape file-id id-new-component keep-touched?]
|
||||
@ -1017,7 +1045,7 @@
|
||||
update-layout-ids (concat update-layout-ids new-children-ids)]
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(dch/commit-changes (fix-font-variants changes))
|
||||
(dwtp/propagate-workspace-tokens) ;; Make the new instance get the token values from the current file, not from the component's library
|
||||
(when (and (features/active-feature? state "render-wasm/v1")
|
||||
(seq new-text-ids))
|
||||
@ -1139,15 +1167,16 @@
|
||||
libraries (dsh/lookup-libraries state)
|
||||
current-file-id (:current-file-id state)
|
||||
|
||||
changes (cll/generate-sync-file-changes
|
||||
(pcb/empty-changes it)
|
||||
undo-group
|
||||
asset-type
|
||||
file-id
|
||||
asset-id
|
||||
library-id
|
||||
libraries
|
||||
current-file-id)
|
||||
changes (-> (cll/generate-sync-file-changes
|
||||
(pcb/empty-changes it)
|
||||
undo-group
|
||||
asset-type
|
||||
file-id
|
||||
asset-id
|
||||
library-id
|
||||
libraries
|
||||
current-file-id)
|
||||
(fix-font-variants))
|
||||
|
||||
find-frames (fn [change]
|
||||
(->> (ch/frames-changed ldata change)
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
;; 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.data.workspace-font-variants-test
|
||||
"A sync that takes a copy text's family from the copy and its weight from
|
||||
the main gives each run a variant of its own family: the closest to its
|
||||
weight and style."
|
||||
(:require
|
||||
[app.common.test-helpers.composable.core :as tm]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.test-helpers.texts :as cthtx]
|
||||
[app.common.types.text :as cttx]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.fonts :as fonts]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.composable-tests.interpreter :as ftm]))
|
||||
|
||||
(def ^:private test-font
|
||||
{:id "test-font"
|
||||
:family "Test"
|
||||
:name "Test"
|
||||
:variants [{:id "400" :name "400" :weight "400" :style "normal"}
|
||||
{:id "700" :name "700" :weight "700" :style "normal"}]})
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before (fn []
|
||||
((:before ftm/fixture))
|
||||
(fonts/register! :test-fonts [test-font]))
|
||||
:after (fn []
|
||||
(fonts/register! :test-fonts [])
|
||||
((:after ftm/fixture)))})
|
||||
|
||||
(def ^:private in-test-font
|
||||
{:font-family "Test" :font-id "test-font"})
|
||||
|
||||
(defn- run-variants
|
||||
[label]
|
||||
(->> (cttx/node-seq cttx/is-text-node? (:content (cths/get-shape (ftm/current-file) label)))
|
||||
(mapv (juxt :text :font-variant-id))))
|
||||
|
||||
(t/deftest fix-font-variants-picks-the-family-closest
|
||||
(let [content [[["Price: " (assoc in-test-font :font-variant-id "regular")]
|
||||
["$10" (assoc in-test-font :weight 700 :font-variant-id "bold")]
|
||||
["!" {:font-variant-id "bold" :weight 700}]]]
|
||||
changes {:redo-changes [{:type :mod-obj
|
||||
:id (cthi/new-id! :text)
|
||||
:operations [{:type :set :attr :content :val (cthtx/content content)}
|
||||
{:type :set :attr :name :val "Price"}]}]}
|
||||
fixed (-> (dwl/fix-font-variants changes) :redo-changes first :operations)]
|
||||
(t/is (= [["Price: " "400"] ["$10" "700"] ["!" "bold"]]
|
||||
(mapv (juxt :text :font-variant-id)
|
||||
(cttx/node-seq cttx/is-text-node? (:val (first fixed))))))
|
||||
(t/is (= {:type :set :attr :name :val "Price"} (second fixed)))))
|
||||
|
||||
(t/deftest ^:async a-sync-gives-the-copy-family-variants
|
||||
(ftm/install! (tm/make-situation
|
||||
(-> (cthf/sample-file :file1)
|
||||
(cthtx/add-text-component-with-copy [[["Price: "] ["$10" {:weight 700}]]])
|
||||
;; the copy changes its family, as the editor does
|
||||
(cthtx/set-text :copy-child [[["Price: " (assoc in-test-font :font-variant-id "400")]
|
||||
["$10" (assoc in-test-font :weight 700 :font-variant-id "700")]]]))))
|
||||
(t/is (= [["Price: " "400"] ["$10" "700"]] (run-variants :copy-child)))
|
||||
;; the main makes all of it bold: the weight comes from the main, with a
|
||||
;; variant of the main's family ("bold")
|
||||
(t/is (= :settled (await (ftm/await-step [(dwsh/update-shapes #{(cthi/id :main-child)}
|
||||
#(assoc % :content (cthtx/content [[["Price: " {:weight 700}]
|
||||
["$10" {:weight 700}]]])))]))))
|
||||
(t/is (= [["Price: $10" "700"]] (run-variants :copy-child))))
|
||||
@ -33,6 +33,7 @@
|
||||
[frontend-tests.data.workspace-context-menu-test]
|
||||
[frontend-tests.data.workspace-edit-watchers-test]
|
||||
[frontend-tests.data.workspace-interactions-test]
|
||||
[frontend-tests.data.workspace-font-variants-test]
|
||||
[frontend-tests.data.workspace-libraries-test]
|
||||
[frontend-tests.data.workspace-library-links-test]
|
||||
[frontend-tests.data.workspace-library-updates-test]
|
||||
@ -201,6 +202,7 @@
|
||||
'frontend-tests.data.workspace-context-menu-test
|
||||
'frontend-tests.data.workspace-edit-watchers-test
|
||||
'frontend-tests.data.workspace-interactions-test
|
||||
'frontend-tests.data.workspace-font-variants-test
|
||||
'frontend-tests.data.workspace-libraries-test
|
||||
'frontend-tests.data.workspace-library-links-test
|
||||
'frontend-tests.data.workspace-library-updates-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user