0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-01 01:21:21 -05:00

Performance improvements

This commit is contained in:
alonso.torres 2021-01-22 16:18:29 +01:00
parent ea03477e8e
commit 6a077c967a
6 changed files with 289 additions and 242 deletions

View file

@ -9,71 +9,68 @@
(ns app.common.attrs) (ns app.common.attrs)
;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes,
;; wll take this value. If there is any shape that is different,
;; the value of the attribute will be the keyword :multiple.
;;
;; If some shape has the value nil in any attribute, it's
;; considered a different value. If the shape does not contain
;; the attribute, it's ignored in the final result.
;;
;; Example:
;; (def shapes [{:stroke-color "#ff0000"
;; :stroke-width 3
;; :fill-color "#0000ff"
;; :x 1000 :y 2000 :rx nil}
;; {:stroke-width "#ff0000"
;; :stroke-width 5
;; :x 1500 :y 2000}])
;;
;; (get-attrs-multi shapes [:stroke-color
;; :stroke-width
;; :fill-color
;; :rx
;; :ry])
;; >>> {:stroke-color "#ff0000"
;; :stroke-width :multiple
;; :fill-color "#0000ff"
;; :rx nil
;; :ry nil}
;;
(defn get-attrs-multi (defn get-attrs-multi
([shapes attrs] (get-attrs-multi shapes attrs = identity)) ([objs attrs]
([shapes attrs eq-fn sel-fn] (get-attrs-multi objs attrs = identity))
;; Extract some attributes of a list of shapes.
;; For each attribute, if the value is the same in all shapes,
;; wll take this value. If there is any shape that is different,
;; the value of the attribute will be the keyword :multiple.
;;
;; If some shape has the value nil in any attribute, it's
;; considered a different value. If the shape does not contain
;; the attribute, it's ignored in the final result.
;;
;; Example:
;; (def shapes [{:stroke-color "#ff0000"
;; :stroke-width 3
;; :fill-color "#0000ff"
;; :x 1000 :y 2000 :rx nil}
;; {:stroke-width "#ff0000"
;; :stroke-width 5
;; :x 1500 :y 2000}])
;;
;; (get-attrs-multi shapes [:stroke-color
;; :stroke-width
;; :fill-color
;; :rx
;; :ry])
;; >>> {:stroke-color "#ff0000"
;; :stroke-width :multiple
;; :fill-color "#0000ff"
;; :rx nil
;; :ry nil}
;;
(let [defined-shapes (filter some? shapes)
combine-value (fn [v1 v2] ([objs attrs eqfn sel]
(cond
(and (= v1 :undefined) (= v2 :undefined)) :undefined
(= v1 :undefined) (if (= v2 :multiple) :multiple (sel-fn v2))
(= v2 :undefined) (if (= v1 :multiple) :multiple (sel-fn v1))
(or (= v1 :multiple) (= v2 :multiple)) :multiple
(eq-fn v1 v2) (sel-fn v1)
:else :multiple))
combine-values (fn [attrs shape values] (loop [attr (first attrs)
(map #(combine-value (get shape % :undefined) attrs (rest attrs)
(get values % :undefined)) attrs)) result (transient {})]
select-attrs (fn [shape attrs] (if attr
(zipmap attrs (map #(get shape % :undefined) attrs))) (let [value
(loop [curr (first objs)
objs (rest objs)
value ::undefined]
reducer (fn [result shape] (if (and curr (not= value :multiple))
(zipmap attrs (combine-values attrs shape result))) ;;
(let [new-val (get curr attr ::undefined)
value (cond
(= new-val ::undefined) value
(= value ::undefined) (sel new-val)
(eqfn new-val value) value
:else :multiple)]
(recur (first objs) (rest objs) value))
;;
value))]
(recur (first attrs)
(rest attrs)
(cond-> result
(not= value ::undefined)
(assoc! attr value))))
combined (reduce reducer (persistent! result)))))
(select-attrs (first defined-shapes) attrs)
(rest defined-shapes))
cleanup-value (fn [value]
(if (= value :undefined) nil value))
cleanup (fn [result]
(->> attrs
(map #(get result %))
(zipmap attrs)
(filter #(not= (second %) :undefined))
(into {})))]
(cleanup combined))))

View file

@ -124,9 +124,6 @@
[shape {:keys [x y]}] [shape {:keys [x y]}]
(move shape (gpt/point (- x) (- y)))) (move shape (gpt/point (- x) (- y))))
(defn translate-from-frame
[shape {:keys [x y]}]
(move shape (gpt/point x y)))
;; --- Helpers ;; --- Helpers

View file

@ -18,64 +18,89 @@
[app.util.object :as obj] [app.util.object :as obj]
[app.util.color :as uc] [app.util.color :as uc]
[app.main.ui.shapes.text.styles :as sts] [app.main.ui.shapes.text.styles :as sts]
[app.main.ui.shapes.text.embed :as ste])) [app.main.ui.shapes.text.embed :as ste]
[app.util.perf :as perf]))
(mf/defc render-text
{::mf/wrap-props false}
[props]
(let [node (obj/get props "node")
text (:text node)
style (sts/generate-text-styles props)]
[:span {:style style
:className (when (:fill-color-gradient node) "gradient")}
(if (= text "") "\u00A0" text)]))
(mf/defc render-root
{::mf/wrap-props false}
[props]
(let [node (obj/get props "node")
embed-fonts? (obj/get props "embed-fonts?")
children (obj/get props "children")
style (sts/generate-root-styles props)]
[:div.root.rich-text
{:style style
:xmlns "http://www.w3.org/1999/xhtml"}
[:*
[:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
(when embed-fonts?
[ste/embed-fontfaces-style {:node node}])]
children]))
(mf/defc render-paragraph-set
{::mf/wrap-props false}
[props]
(let [node (obj/get props "node")
children (obj/get props "children")
style (sts/generate-paragraph-set-styles props)]
[:div.paragraph-set {:style style} children]))
(mf/defc render-paragraph
{::mf/wrap-props false}
[props]
(let [node (obj/get props "node")
children (obj/get props "children")
style (sts/generate-paragraph-styles props)]
[:p.paragraph {:style style} children]))
;; -- Text nodes ;; -- Text nodes
(mf/defc text-node (mf/defc render-node
[{:keys [node index shape] :as props}] {::mf/wrap-props false}
(let [embed-resources? (mf/use-ctx muc/embed-ctx) [props]
{:keys [type text children]} node (let [node (obj/get props "node")
props #js {:shape shape} index (obj/get props "index")
render-node {:keys [type text children]} node]
(fn [index node]
(mf/element text-node {:index index
:node node
:key index
:shape shape}))]
(if (string? text) (if (string? text)
(let [style (sts/generate-text-styles (clj->js node) props)] [:> render-text props]
[:span {:style style
:className (when (:fill-color-gradient node) "gradient")}
(if (= text "") "\u00A0" text)])
(let [children (map-indexed render-node children)] (let [component (case type
(case type "root" render-root
"root" "paragraph-set" render-paragraph-set
(let [style (sts/generate-root-styles (clj->js node) props)] "paragraph" render-paragraph
[:div.root.rich-text nil)]
{:key index (when component
:style style [:> component (obj/set! props "key" index)
:xmlns "http://www.w3.org/1999/xhtml"} (for [[index child] (d/enumerate children)]
[:* (let [props (-> props
[:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] (obj/set! "node" child)
(when embed-resources? (obj/set! "index" index))]
[ste/embed-fontfaces-style {:node node}])] [:> render-node props]))])))))
children])
"paragraph-set"
(let [style (sts/generate-paragraph-set-styles (clj->js node) props)]
[:div.paragraph-set {:key index :style style} children])
"paragraph"
(let [style (sts/generate-paragraph-styles (clj->js node) props)]
[:p.paragraph {:key index :style style} children])
nil)))))
(mf/defc text-content (mf/defc text-content
{::mf/wrap-props false {::mf/wrap-props false}
::mf/wrap [mf/memo]}
[props] [props]
(let [root (obj/get props "content") (let [root (obj/get props "content")
shape (obj/get props "shape")] shape (obj/get props "shape")
[:& text-node {:index 0 embed-fonts? (obj/get props "embed-fonts?")]
:node root [:& render-node {:index 0
:shape shape}])) :node root
:shape shape
:embed-fonts? embed-fonts?}]))
(defn- retrieve-colors (defn- retrieve-colors
[shape] [shape]
(let [colors (->> shape :content (let [colors (->> shape
:content
(tree-seq map? :children) (tree-seq map? :children)
(into #{} (comp (map :fill-color) (filter string?))))] (into #{} (comp (map :fill-color) (filter string?))))]
(if (empty? colors) (if (empty? colors)
@ -87,8 +112,8 @@
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [props ref]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
selected? (unchecked-get props "selected?")
grow-type (unchecked-get props "grow-type") grow-type (unchecked-get props "grow-type")
embed-fonts? (mf/use-ctx muc/embed-ctx)
{:keys [id x y width height content]} shape] {:keys [id x y width height content]} shape]
[:foreignObject {:x x [:foreignObject {:x x
:y y :y y
@ -99,4 +124,5 @@
:height (if (#{:auto-height :auto-width} grow-type) 10000 height) :height (if (#{:auto-height :auto-width} grow-type) 10000 height)
:ref ref} :ref ref}
[:& text-content {:shape shape [:& text-content {:shape shape
:content (:content shape)}]])) :content (:content shape)
:embed-fonts? embed-fonts?}]]))

View file

@ -17,111 +17,115 @@
[app.util.text :as ut])) [app.util.text :as ut]))
(defn generate-root-styles (defn generate-root-styles
[data props] ([props] (generate-root-styles (clj->js (obj/get props "node")) props))
(let [valign (obj/get data "vertical-align" "top") ([data props]
talign (obj/get data "text-align" "flex-start") (let [valign (obj/get data "vertical-align" "top")
shape (obj/get props "shape") talign (obj/get data "text-align" "flex-start")
base #js {:height (or (:height shape) "100%") shape (obj/get props "shape")
:width (or (:width shape) "100%") base #js {:height (or (:height shape) "100%")
:display "flex"}] :width (or (:width shape) "100%")
(cond-> base :display "flex"}]
(= valign "top") (obj/set! "alignItems" "flex-start") (cond-> base
(= valign "center") (obj/set! "alignItems" "center") (= valign "top") (obj/set! "alignItems" "flex-start")
(= valign "bottom") (obj/set! "alignItems" "flex-end") (= valign "center") (obj/set! "alignItems" "center")
(= valign "bottom") (obj/set! "alignItems" "flex-end")
(= talign "left") (obj/set! "justifyContent" "flex-start") (= talign "left") (obj/set! "justifyContent" "flex-start")
(= talign "center") (obj/set! "justifyContent" "center") (= talign "center") (obj/set! "justifyContent" "center")
(= talign "right") (obj/set! "justifyContent" "flex-end") (= talign "right") (obj/set! "justifyContent" "flex-end")
(= talign "justify") (obj/set! "justifyContent" "stretch")))) (= talign "justify") (obj/set! "justifyContent" "stretch")))))
(defn generate-paragraph-set-styles (defn generate-paragraph-set-styles
[data props] ([props] (generate-paragraph-set-styles nil props))
;; The position absolute is used so the paragraph is "outside" ([data props]
;; the normal layout and can grow outside its parent ;; The position absolute is used so the paragraph is "outside"
;; We use this element to measure the size of the text ;; the normal layout and can grow outside its parent
(let [base #js {:display "inline-block"}] ;; We use this element to measure the size of the text
base)) (let [base #js {:display "inline-block"}]
base)))
(defn generate-paragraph-styles (defn generate-paragraph-styles
[data props] ([props] (generate-paragraph-styles (clj->js (obj/get props "node")) props))
(let [shape (obj/get props "shape") ([data props]
grow-type (:grow-type shape) (let [shape (obj/get props "shape")
base #js {:fontSize "14px" grow-type (:grow-type shape)
:margin "inherit" base #js {:fontSize "14px"
:lineHeight "1.2"} :margin "inherit"
lh (obj/get data "line-height") :lineHeight "1.2"}
ta (obj/get data "text-align")] lh (obj/get data "line-height")
(cond-> base ta (obj/get data "text-align")]
ta (obj/set! "textAlign" ta) (cond-> base
lh (obj/set! "lineHeight" lh) ta (obj/set! "textAlign" ta)
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))) lh (obj/set! "lineHeight" lh)
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))))
(defn generate-text-styles (defn generate-text-styles
[data props] ([props] (generate-text-styles (clj->js (obj/get props "node")) props))
(let [letter-spacing (obj/get data "letter-spacing") ([data props]
text-decoration (obj/get data "text-decoration") (let [letter-spacing (obj/get data "letter-spacing")
text-transform (obj/get data "text-transform") text-decoration (obj/get data "text-decoration")
line-height (obj/get data "line-height") text-transform (obj/get data "text-transform")
line-height (obj/get data "line-height")
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs)) font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
font-variant-id (obj/get data "font-variant-id") font-variant-id (obj/get data "font-variant-id")
font-family (obj/get data "font-family") font-family (obj/get data "font-family")
font-size (obj/get data "font-size") font-size (obj/get data "font-size")
;; Old properties for backwards compatibility ;; Old properties for backwards compatibility
fill (obj/get data "fill") fill (obj/get data "fill")
opacity (obj/get data "opacity" 1) opacity (obj/get data "opacity" 1)
fill-color (obj/get data "fill-color" fill) fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity) fill-opacity (obj/get data "fill-opacity" opacity)
fill-color-gradient (obj/get data "fill-color-gradient" nil) fill-color-gradient (obj/get data "fill-color-gradient" nil)
fill-color-gradient (when fill-color-gradient fill-color-gradient (when fill-color-gradient
(-> (js->clj fill-color-gradient :keywordize-keys true) (-> (js->clj fill-color-gradient :keywordize-keys true)
(update :type keyword))) (update :type keyword)))
;; Uncomment this to allow to remove text colors. This could break the texts that already exist ;; Uncomment this to allow to remove text colors. This could break the texts that already exist
;;[r g b a] (if (nil? fill-color) ;;[r g b a] (if (nil? fill-color)
;; [0 0 0 0] ;; Transparent color ;; [0 0 0 0] ;; Transparent color
;; (uc/hex->rgba fill-color fill-opacity)) ;; (uc/hex->rgba fill-color fill-opacity))
[r g b a] (uc/hex->rgba fill-color fill-opacity) [r g b a] (uc/hex->rgba fill-color fill-opacity)
text-color (if fill-color-gradient text-color (if fill-color-gradient
(uc/gradient->css (js->clj fill-color-gradient)) (uc/gradient->css (js->clj fill-color-gradient))
(str/format "rgba(%s, %s, %s, %s)" r g b a)) (str/format "rgba(%s, %s, %s, %s)" r g b a))
fontsdb (deref fonts/fontsdb) fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration base #js {:textDecoration text-decoration
:textTransform text-transform :textTransform text-transform
:lineHeight (or line-height "inherit") :lineHeight (or line-height "inherit")
:color text-color :color text-color
"--text-color" text-color}] "--text-color" text-color}]
(when (and (string? letter-spacing) (when (and (string? letter-spacing)
(pos? (alength letter-spacing))) (pos? (alength letter-spacing)))
(obj/set! base "letterSpacing" (str letter-spacing "px"))) (obj/set! base "letterSpacing" (str letter-spacing "px")))
(when (and (string? font-size) (when (and (string? font-size)
(pos? (alength font-size))) (pos? (alength font-size)))
(obj/set! base "fontSize" (str font-size "px"))) (obj/set! base "fontSize" (str font-size "px")))
(when (and (string? font-id) (when (and (string? font-id)
(pos? (alength font-id))) (pos? (alength font-id)))
(fonts/ensure-loaded! font-id) (fonts/ensure-loaded! font-id)
(let [font (get fontsdb font-id)] (let [font (get fontsdb font-id)]
(let [font-family (or (:family font) (let [font-family (or (:family font)
(obj/get data "fontFamily")) (obj/get data "fontFamily"))
font-variant (d/seek #(= font-variant-id (:id %)) font-variant (d/seek #(= font-variant-id (:id %))
(:variants font)) (:variants font))
font-style (or (:style font-variant) font-style (or (:style font-variant)
(obj/get data "fontStyle")) (obj/get data "fontStyle"))
font-weight (or (:weight font-variant) font-weight (or (:weight font-variant)
(obj/get data "fontWeight"))] (obj/get data "fontWeight"))]
(obj/set! base "fontFamily" font-family) (obj/set! base "fontFamily" font-family)
(obj/set! base "fontStyle" font-style) (obj/set! base "fontStyle" font-style)
(obj/set! base "fontWeight" font-weight)))) (obj/set! base "fontWeight" font-weight))))
base)) base)))

View file

@ -34,46 +34,33 @@
;; --- Events ;; --- Events
(defn use-double-click [{:keys [id]} selected?] (defn use-double-click [{:keys [id]}]
(mf/use-callback (mf/use-callback
(mf/deps id selected?) (mf/deps id)
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(dom/prevent-default event) (dom/prevent-default event)
(when selected? (st/emit! (dw/start-edition-mode id)))))
(st/emit! (dw/start-edition-mode id))))))
;; --- Text Wrapper for workspace ;; --- Text Wrapper for workspace
(mf/defc text-wrapper (mf/defc text-static-content
[{:keys [shape]}]
[:& text/text-shape {:shape shape
:grow-type (:grow-type shape)}])
(mf/defc text-resize-content
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [{:keys [id name x y width height grow-type] :as shape} (unchecked-get props "shape") (let [shape (obj/get props "shape")
ghost? (mf/use-ctx muc/ghost-ctx) {:keys [id name x y grow-type]} shape
selected-iref (mf/use-memo (mf/deps (:id shape))
#(refs/make-selected-ref (:id shape)))
selected? (mf/deref selected-iref)
edition (mf/deref refs/selected-edition)
current-transform (mf/deref refs/current-transform)
render-editor (mf/use-state false)
edition? (= edition id)
embed-resources? (mf/use-ctx muc/embed-ctx)
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
handle-double-click (use-double-click shape selected?)
paragraph-ref (mf/use-state nil) paragraph-ref (mf/use-state nil)
handle-resize-text handle-resize-text
(mf/use-callback (mf/use-callback
(mf/deps id) (mf/deps id)
(fn [entries] (fn [entries]
(when (and (not ghost?) (seq entries)) (when (seq entries)
;; RequestAnimationFrame so the "loop limit error" error is not thrown ;; RequestAnimationFrame so the "loop limit error" error is not thrown
;; https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded ;; https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
(timers/raf (timers/raf
@ -97,24 +84,41 @@
(mf/use-effect (mf/use-effect
(mf/deps @paragraph-ref handle-resize-text grow-type) (mf/deps @paragraph-ref handle-resize-text grow-type)
(fn [] (fn []
(when (not ghost?) (when-let [paragraph-node @paragraph-ref]
(when-let [paragraph-node @paragraph-ref] (let [observer (js/ResizeObserver. handle-resize-text)]
(let [observer (js/ResizeObserver. handle-resize-text)] (log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name) (.observe observer paragraph-node)
(.observe observer paragraph-node) #(.disconnect observer)))))
#(.disconnect observer))))))
[:& text/text-shape {:ref text-ref-cb
:shape shape
:grow-type (:grow-type shape)}]))
(mf/defc text-wrapper
{::mf/wrap-props false}
[props]
(let [{:keys [id x y width height] :as shape} (unchecked-get props "shape")
ghost? (mf/use-ctx muc/ghost-ctx)
edition (mf/deref refs/selected-edition)
edition? (= edition id)
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
handle-double-click (use-double-click shape)]
[:> shape-container {:shape shape} [:> shape-container {:shape shape}
;; We keep hidden the shape when we're editing so it keeps track of the size ;; We keep hidden the shape when we're editing so it keeps track of the size
;; and updates the selrect acordingly ;; and updates the selrect acordingly
[:g.text-shape {:opacity (when edition? 0) [:g.text-shape {:opacity (when edition? 0)
:pointer-events "none"} :pointer-events "none"}
[:& text/text-shape {:key (str "text-shape" (:id shape))
:ref text-ref-cb (if ghost?
:shape shape [:& text-static-content {:shape shape}]
:selected? selected? [:& text-resize-content {:shape shape}])]
:grow-type (:grow-type shape)}]]
(when (and (not ghost?) edition?) (when (and (not ghost?) edition?)
[:& editor/text-shape-edit {:key (str "editor" (:id shape)) [:& editor/text-shape-edit {:key (str "editor" (:id shape))
:shape shape}]) :shape shape}])

View file

@ -93,12 +93,31 @@
(rec-fn {} node))) (rec-fn {} node)))
(defn content->nodes [node]
(loop [result (transient [])
curr node
pending (transient [])]
(let [result (conj! result curr)]
;; Adds children to the pending list
(let [children (:children curr)
pending (loop [child (first children)
children (rest children)
pending pending]
(if child
(recur (first children)
(rest children)
(conj! pending child))
pending))]
(if (= 0 (count pending))
(persistent! result)
;; Iterates with the next value in pending
(let [next (get pending (dec (count pending)))]
(recur result next (pop! pending))))))))
(defn get-text-attrs-multi (defn get-text-attrs-multi
[node attrs] [node attrs]
(let [rec-fn (let [nodes (content->nodes node)]
(fn rec-fn [current node] (get-attrs-multi nodes attrs)))
(let [current (reduce rec-fn current (:children node []))]
(get-attrs-multi [current node] attrs)))]
(merge (select-keys default-text-attrs attrs)
(rec-fn {} node))))