mirror of
https://github.com/penpot/penpot.git
synced 2025-03-13 16:21:57 -05:00
⚡ Performance improvements
This commit is contained in:
parent
814042909a
commit
b576ef02af
16 changed files with 510 additions and 315 deletions
|
@ -248,11 +248,20 @@
|
|||
(dm/get-in data [:pages-index page-id])))
|
||||
st/state))
|
||||
|
||||
(defn workspace-page-objects-by-id
|
||||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
|
||||
(def workspace-page-objects
|
||||
(l/derived wsh/lookup-page-objects st/state =))
|
||||
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
(defn object-by-id
|
||||
[id]
|
||||
(l/derived #(get % id) workspace-page-objects))
|
||||
|
||||
(defn objects-by-id
|
||||
[ids]
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =))
|
||||
|
||||
(def workspace-page-options
|
||||
(l/derived :options workspace-page))
|
||||
|
@ -266,13 +275,35 @@
|
|||
(def workspace-editor-state
|
||||
(l/derived :workspace-editor-state st/state))
|
||||
|
||||
(defn object-by-id
|
||||
[id]
|
||||
(l/derived #(get % id) workspace-page-objects))
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(defn objects-by-id
|
||||
(defn workspace-modifiers-by-id
|
||||
[ids]
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects))
|
||||
(l/derived #(select-keys % ids) workspace-modifiers))
|
||||
|
||||
|
||||
(def workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
(fn [state]
|
||||
{:modifiers (:workspace-modifiers state)
|
||||
:objects (wsh/lookup-page-objects state)})
|
||||
st/state
|
||||
(fn [a b]
|
||||
(and (= (:modifiers a) (:modifiers b))
|
||||
(identical? (:objects a) (:objects b))))))
|
||||
|
||||
(defn workspace-modifiers-by-frame-id
|
||||
[frame-id]
|
||||
(l/derived
|
||||
(fn [{:keys [modifiers objects]}]
|
||||
(let [keys (->> modifiers
|
||||
(keys)
|
||||
(filter #(or (= frame-id %)
|
||||
(= frame-id (get-in objects [% :frame-id])))))]
|
||||
(select-keys modifiers keys)))
|
||||
workspace-modifiers-with-objects
|
||||
=))
|
||||
|
||||
(defn- set-content-modifiers [state]
|
||||
(fn [id shape]
|
||||
|
|
|
@ -243,8 +243,9 @@
|
|||
|
||||
(let [shapes (->> shapes
|
||||
(remove cph/frame-shape?)
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))
|
||||
fonts (ff/shapes->fonts shapes)]
|
||||
[:& ff/fontfaces-style {:fonts fonts}])
|
||||
|
||||
(for [item shapes]
|
||||
(let [frame? (= (:type item) :frame)]
|
||||
|
@ -401,8 +402,8 @@
|
|||
:style {:-webkit-print-color-adjust :exact}
|
||||
:fill "none"}
|
||||
|
||||
(let [shapes (cph/get-children objects object-id)]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
(let [fonts (ff/frame->fonts obj-id objects)]
|
||||
[:& ff/fontfaces-style {:fonts fonts}])
|
||||
|
||||
(case (:type object)
|
||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||
|
|
|
@ -213,6 +213,15 @@
|
|||
(mf/set-ref-val! ref value)))
|
||||
(mf/ref-val ref)))
|
||||
|
||||
(defn use-update-var
|
||||
[value]
|
||||
(let [ref (mf/use-var value)]
|
||||
(mf/use-effect
|
||||
(mf/deps value)
|
||||
(fn []
|
||||
(reset! ref value)))
|
||||
ref))
|
||||
|
||||
(defn use-equal-memo
|
||||
[val]
|
||||
(let [ref (mf/use-ref nil)]
|
||||
|
@ -248,3 +257,5 @@
|
|||
(mf/deps focus objects)
|
||||
#(cp/focus-objects objects focus))]
|
||||
objects)))
|
||||
|
||||
|
||||
|
|
|
@ -46,7 +46,10 @@
|
|||
:characterData true}
|
||||
mutation-obs (js/MutationObserver. on-mutation)]
|
||||
(mf/set-ref-val! prev-obs-ref mutation-obs)
|
||||
(.observe mutation-obs node options))))))]
|
||||
(.observe mutation-obs node options))))
|
||||
|
||||
;; Return node so it's more composable
|
||||
node))]
|
||||
|
||||
(mf/with-effect
|
||||
(fn []
|
||||
|
|
|
@ -63,13 +63,14 @@
|
|||
(let [childs (unchecked-get props "childs")
|
||||
shape (unchecked-get props "shape")
|
||||
{:keys [x y width height]} shape
|
||||
|
||||
transform (gsh/transform-matrix shape)
|
||||
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
#js {:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:transform (str transform)
|
||||
:width width
|
||||
:height height
|
||||
:className "frame-background"}))
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children (unchecked-get props "children")
|
||||
|
||||
{:keys [x y width height]} shape
|
||||
{:keys [attrs] :as content} (:content shape)
|
||||
|
||||
|
@ -61,7 +60,7 @@
|
|||
(obj/set! "preserveAspectRatio" "none"))]
|
||||
|
||||
[:& (mf/provider svg-ids-ctx) {:value ids-mapping}
|
||||
[:g.svg-raw {:transform (gsh/transform-matrix shape)}
|
||||
[:g.svg-raw {:transform (str (gsh/transform-matrix shape))}
|
||||
[:> "svg" attrs children]]]))
|
||||
|
||||
(mf/defc svg-element
|
||||
|
|
|
@ -73,16 +73,25 @@
|
|||
(when (d/not-empty? style)
|
||||
[:style style])))
|
||||
|
||||
(defn frame->fonts
|
||||
[frame objects]
|
||||
(->> (cph/get-children objects (:id frame))
|
||||
(filterv cph/text-shape?)
|
||||
(mapv (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})))
|
||||
|
||||
(defn shapes->fonts
|
||||
[shapes]
|
||||
(->> shapes
|
||||
(filterv cph/text-shape?)
|
||||
(mapv (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})))
|
||||
|
||||
(mf/defc fontfaces-style
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]}
|
||||
[props]
|
||||
(let [;; Retrieve the fonts ids used by the text shapes
|
||||
fonts (->> (obj/get props "shapes")
|
||||
(filterv cph/text-shape?)
|
||||
(mapv (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})
|
||||
(hooks/use-equal-memo))]
|
||||
|
||||
fonts (obj/get props "fonts")]
|
||||
(when (d/not-empty? fonts)
|
||||
[:> fontfaces-style-render {:fonts fonts}])))
|
||||
|
|
|
@ -67,9 +67,8 @@
|
|||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
(let [shape (obj/get props "shape")
|
||||
opts #js {:shape shape}]
|
||||
|
||||
(when (and (some? shape) (not (:hidden shape)))
|
||||
[:*
|
||||
(case (:type shape)
|
||||
|
|
|
@ -8,11 +8,17 @@
|
|||
(:require
|
||||
[app.common.colors :as cc]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.core :as rx]
|
||||
|
@ -75,56 +81,268 @@
|
|||
(mf/jsx component props mf/undefined)
|
||||
(mf/jsx frame-placeholder props mf/undefined)))))
|
||||
|
||||
;; Draw the frame proper as a deferred component
|
||||
(defn deferred-frame-shape-factory
|
||||
(defn use-node-store
|
||||
[thumbnail? node-ref rendered?]
|
||||
|
||||
(let [;; when `true` the node is in memory
|
||||
in-memory? (mf/use-var nil)
|
||||
|
||||
;; State just for re-rendering
|
||||
re-render (mf/use-state 0)
|
||||
|
||||
parent-ref (mf/use-var nil)
|
||||
|
||||
on-frame-load
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when (and (some? node) (nil? @node-ref))
|
||||
(let [content (.createElementNS globals/document "http://www.w3.org/2000/svg" "g")]
|
||||
(.appendChild node content)
|
||||
(reset! node-ref content)
|
||||
(reset! parent-ref node)
|
||||
(swap! re-render inc)))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thumbnail?)
|
||||
(fn []
|
||||
(when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?)
|
||||
(.removeChild @parent-ref @node-ref)
|
||||
(reset! in-memory? true))
|
||||
|
||||
(when (and (some? @node-ref) @in-memory? (not thumbnail?))
|
||||
(.appendChild @parent-ref @node-ref)
|
||||
(reset! in-memory? false))))
|
||||
|
||||
on-frame-load))
|
||||
|
||||
(defn use-render-thumbnail
|
||||
[{:keys [x y width height] :as shape} node-ref rendered? thumbnail? thumbnail-data]
|
||||
|
||||
(let [frame-canvas-ref (mf/use-ref nil)
|
||||
frame-image-ref (mf/use-ref nil)
|
||||
|
||||
fixed-width (mth/clamp (:width shape) 250 2000)
|
||||
fixed-height (/ (* (:height shape) fixed-width) (:width shape))
|
||||
|
||||
image-url (mf/use-state nil)
|
||||
observer-ref (mf/use-var nil)
|
||||
|
||||
shape-ref (hooks/use-update-var shape)
|
||||
|
||||
on-image-load
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(let [canvas-node (mf/ref-val frame-canvas-ref)
|
||||
img-node (mf/ref-val frame-image-ref)
|
||||
|
||||
canvas-context (.getContext canvas-node "2d")
|
||||
canvas-width (.-width canvas-node)
|
||||
canvas-height (.-height canvas-node)]
|
||||
(.clearRect canvas-context 0 0 canvas-width canvas-height)
|
||||
(.rect canvas-context 0 0 canvas-width canvas-height)
|
||||
(set! (.-fillStyle canvas-context) "#FFFFFF")
|
||||
(.fill canvas-context)
|
||||
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
||||
|
||||
(let [data (.toDataURL canvas-node "image/jpg" 1)]
|
||||
(reset! thumbnail-data data))
|
||||
(reset! image-url nil))))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(when (some? @node-ref)
|
||||
(let [node @node-ref]
|
||||
(ts/schedule-on-idle
|
||||
#(let [frame-html (-> (js/XMLSerializer.)
|
||||
(.serializeToString node))
|
||||
|
||||
{:keys [x y width height]} @shape-ref
|
||||
svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg")
|
||||
_ (.setAttribute svg-node "version" "1.1")
|
||||
_ (.setAttribute svg-node "viewBox" (dm/str x " " y " " width " " height))
|
||||
_ (.setAttribute svg-node "width" width)
|
||||
_ (.setAttribute svg-node "height" height)
|
||||
_ (unchecked-set svg-node "innerHTML" frame-html)
|
||||
|
||||
xml (-> (js/XMLSerializer.)
|
||||
(.serializeToString svg-node)
|
||||
js/encodeURIComponent
|
||||
js/unescape
|
||||
js/btoa)
|
||||
|
||||
img-src (str "data:image/svg+xml;base64," xml)]
|
||||
(reset! image-url img-src)))))))
|
||||
|
||||
on-load-frame-dom
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when (and (some? node) (nil? @observer-ref))
|
||||
(let [observer (js/MutationObserver. on-change)]
|
||||
(.observe observer node #js {:childList true :attributes true :characterData true :subtree true})
|
||||
(reset! observer-ref observer)))
|
||||
|
||||
;; First time rendered if the thumbnail is not present we create it
|
||||
(when (not thumbnail?) (on-change []))))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
#(when (and (some? @node-ref) @rendered?)
|
||||
(mf/unmount @node-ref)
|
||||
(reset! node-ref nil)
|
||||
(reset! rendered? false)
|
||||
(when (some? @observer-ref)
|
||||
(.disconnect @observer-ref)
|
||||
(reset! observer-ref nil)))))
|
||||
|
||||
[on-load-frame-dom
|
||||
(when (some? @image-url)
|
||||
(mf/html
|
||||
[:g.thumbnail-rendering
|
||||
[:foreignObject {:opacity 0 :x x :y y :width width :height height}
|
||||
[:canvas {:ref frame-canvas-ref
|
||||
:width fixed-width
|
||||
:height fixed-height}]]
|
||||
|
||||
[:image {:opacity 0
|
||||
:ref frame-image-ref
|
||||
:x (:x shape)
|
||||
:y (:y shape)
|
||||
:xlinkHref @image-url
|
||||
:width (:width shape)
|
||||
:height (:height shape)
|
||||
:on-load on-image-load}]]))]))
|
||||
|
||||
(defn use-dynamic-modifiers
|
||||
[shape objects node-ref]
|
||||
|
||||
(let [frame-modifiers-ref
|
||||
(mf/use-memo
|
||||
(mf/deps (:id shape))
|
||||
#(refs/workspace-modifiers-by-frame-id (:id shape)))
|
||||
|
||||
modifiers (mf/deref frame-modifiers-ref)
|
||||
|
||||
transforms
|
||||
(mf/use-memo
|
||||
(mf/deps modifiers)
|
||||
(fn []
|
||||
(when (some? modifiers)
|
||||
(d/mapm (fn [id {modifiers :modifiers}]
|
||||
(let [center (gsh/center-shape (get objects id))]
|
||||
(gsh/modifiers->transform center modifiers)))
|
||||
modifiers))))
|
||||
|
||||
shapes
|
||||
(mf/use-memo
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(->> (keys transforms)
|
||||
(mapv (d/getf objects)))))
|
||||
|
||||
prev-shapes (mf/use-var nil)
|
||||
prev-modifiers (mf/use-var nil)
|
||||
prev-transforms (mf/use-var nil)]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(when (and (nil? @prev-transforms)
|
||||
(some? transforms))
|
||||
(utils/start-transform! @node-ref shapes))
|
||||
|
||||
(when (some? modifiers)
|
||||
(utils/update-transform! @node-ref shapes transforms modifiers))
|
||||
|
||||
(when (and (some? @prev-modifiers)
|
||||
(empty? modifiers))
|
||||
(utils/remove-transform! @node-ref @prev-shapes))
|
||||
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
(reset! prev-shapes shapes)))))
|
||||
|
||||
(defn frame-shape-factory-roots
|
||||
[shape-wrapper]
|
||||
|
||||
(let [frame-shape (frame/frame-shape shape-wrapper)]
|
||||
(mf/fnc defered-frame-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "childs"]))
|
||||
custom-deferred]}
|
||||
(mf/fnc inner-frame-shape
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "childs" "fonts" "thumbnail?"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")]
|
||||
[:& frame-shape {:shape shape
|
||||
:childs childs}]))))
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
fonts (unchecked-get props "fonts")
|
||||
objects (unchecked-get props "objects")
|
||||
|
||||
thumbnail-data (mf/use-state nil)
|
||||
|
||||
thumbnail? (and thumbnail?
|
||||
(or (some? (:thumbnail shape))
|
||||
(some? @thumbnail-data)))
|
||||
|
||||
|
||||
;; References to the current rendered node and the its parentn
|
||||
node-ref (mf/use-var nil)
|
||||
|
||||
;; when `true` we've called the mount for the frame
|
||||
rendered? (mf/use-var false)
|
||||
|
||||
[on-load-frame-dom thumb-renderer]
|
||||
(use-render-thumbnail shape node-ref rendered? thumbnail? thumbnail-data)
|
||||
|
||||
on-frame-load
|
||||
(use-node-store thumbnail? node-ref rendered?)]
|
||||
|
||||
(use-dynamic-modifiers shape objects node-ref)
|
||||
|
||||
(when (and (some? @node-ref) (or @rendered? (not thumbnail?)))
|
||||
(mf/mount
|
||||
(mf/html
|
||||
[:& (mf/provider embed/context) {:value true}
|
||||
[:> shape-container #js {:shape shape :ref on-load-frame-dom}
|
||||
[:& ff/fontfaces-style {:fonts fonts}]
|
||||
[:> frame-shape {:shape shape
|
||||
:childs childs} ]]])
|
||||
@node-ref)
|
||||
(when (not @rendered?) (reset! rendered? true)))
|
||||
|
||||
[:*
|
||||
(when thumbnail?
|
||||
[:> frame/frame-thumbnail {:shape (cond-> shape
|
||||
(some? @thumbnail-data)
|
||||
(assoc :thumbnail @thumbnail-data))}])
|
||||
|
||||
[:g.frame-container {:key "frame-container"
|
||||
:ref on-frame-load}]
|
||||
thumb-renderer]))))
|
||||
|
||||
(defn frame-wrapper-factory
|
||||
[shape-wrapper]
|
||||
(let [deferred-frame-shape (deferred-frame-shape-factory shape-wrapper)]
|
||||
(let [frame-shape (frame-shape-factory-roots shape-wrapper)]
|
||||
(mf/fnc frame-wrapper
|
||||
{::mf/wrap [#(mf/memo' % check-frame-props)]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(when-let [shape (unchecked-get props "shape")]
|
||||
(let [objects (unchecked-get props "objects")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
(let [shape (unchecked-get props "shape")
|
||||
objects (unchecked-get props "objects")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
|
||||
children
|
||||
(-> (mapv (d/getf objects) (:shapes shape))
|
||||
(hooks/use-equal-memo))
|
||||
children
|
||||
(-> (mapv (d/getf objects) (:shapes shape))
|
||||
(hooks/use-equal-memo))
|
||||
|
||||
all-children
|
||||
(-> (cph/get-children objects (:id shape))
|
||||
(hooks/use-equal-memo))
|
||||
|
||||
all-svg-text?
|
||||
(mf/use-memo
|
||||
(mf/deps all-children)
|
||||
(fn []
|
||||
(->> all-children
|
||||
(filter #(and (= :text (:type %)) (not (:hidden %))))
|
||||
(every? #(some? (:position-data %))))))
|
||||
|
||||
show-thumbnail? (and thumbnail? (some? (:thumbnail shape)) all-svg-text?)]
|
||||
|
||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||
[:> shape-container {:shape shape}
|
||||
[:& ff/fontfaces-style {:shapes all-children}]
|
||||
(if show-thumbnail?
|
||||
[:& frame/frame-thumbnail {:shape shape}]
|
||||
[:& deferred-frame-shape
|
||||
{:shape shape
|
||||
:childs children}])]])))))
|
||||
fonts
|
||||
(-> (ff/frame->fonts shape objects)
|
||||
(hooks/use-equal-memo))]
|
||||
|
||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||
[:& frame-shape
|
||||
{:key (str (:id shape))
|
||||
:shape shape
|
||||
:fonts fonts
|
||||
:childs children
|
||||
:objects objects
|
||||
:thumbnail? thumbnail?}]]))))
|
||||
|
|
|
@ -30,9 +30,9 @@
|
|||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
|
||||
childs (mf/deref childs-ref)]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
[:& group-shape
|
||||
|
|
|
@ -6,228 +6,23 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.text
|
||||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fo-text :as fo]
|
||||
[app.main.ui.shapes.text.svg-text :as svg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.svg :as usvg]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as utp]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[debug :refer [debug?]]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; Change this to :info :debug or :trace to debug this module
|
||||
(log/set-level! :warn)
|
||||
|
||||
;; --- Text Wrapper for workspace
|
||||
|
||||
(mf/defc text-static-content
|
||||
[{:keys [shape]}]
|
||||
[:& fo/text-shape {:shape shape
|
||||
:grow-type (:grow-type shape)}])
|
||||
|
||||
(defn- update-with-current-editor-state
|
||||
[{:keys [id] :as shape}]
|
||||
(let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state))
|
||||
editor-state (mf/deref editor-state-ref)
|
||||
|
||||
content (:content shape)
|
||||
editor-content
|
||||
(when editor-state
|
||||
(-> editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content)))]
|
||||
|
||||
(cond-> shape
|
||||
(some? editor-content)
|
||||
(assoc :content (attrs/merge content editor-content)))))
|
||||
|
||||
(mf/defc text-resize-content
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id name grow-type] :as shape} (obj/get props "shape")
|
||||
|
||||
;; NOTE: this breaks the hooks rule of "no hooks inside
|
||||
;; conditional code"; but we ensure that this component will
|
||||
;; not reused if edition flag is changed with `:key` prop.
|
||||
;; Without the `:key` prop combining the shape-id and the
|
||||
;; edition flag, this will result in a react error. This is
|
||||
;; done for performance reason; with this change only the
|
||||
;; shape with edition flag is watching the editor state ref.
|
||||
shape (cond-> shape
|
||||
(true? (obj/get props "edition?"))
|
||||
(update-with-current-editor-state))
|
||||
|
||||
mnt (mf/use-ref true)
|
||||
paragraph-ref (mf/use-state nil)
|
||||
|
||||
handle-resize-text
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn [entries]
|
||||
(when (seq entries)
|
||||
;; RequestAnimationFrame so the "loop limit error" error is not thrown
|
||||
;; https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
||||
(timers/raf
|
||||
#(let [width (obj/get-in entries [0 "contentRect" "width"])
|
||||
height (obj/get-in entries [0 "contentRect" "height"])]
|
||||
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(log/debug :msg "Resize detected" :shape-id id :width width :height height)
|
||||
(st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height)))))))))
|
||||
|
||||
text-ref-cb
|
||||
(mf/use-callback
|
||||
(mf/deps handle-resize-text)
|
||||
(fn [node]
|
||||
(when node
|
||||
(timers/schedule
|
||||
#(when (mf/ref-val mnt)
|
||||
(when-let [ps-node (dom/query node ".paragraph-set")]
|
||||
(reset! paragraph-ref ps-node)))))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @paragraph-ref handle-resize-text grow-type)
|
||||
(fn []
|
||||
(when-let [paragraph-node @paragraph-ref]
|
||||
(let [sub (->> (wapi/observe-resize paragraph-node)
|
||||
(rx/observe-on :af)
|
||||
(rx/subs handle-resize-text))]
|
||||
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
|
||||
(fn []
|
||||
(rx/dispose! sub))))))
|
||||
|
||||
(mf/use-effect
|
||||
(fn [] #(mf/set-ref-val! mnt false)))
|
||||
|
||||
[:& fo/text-shape {:ref text-ref-cb
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)
|
||||
:key (str "shape-" (:id shape))}]))
|
||||
|
||||
|
||||
(mf/defc text-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id position-data] :as shape} (unchecked-get props "shape")
|
||||
edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local))
|
||||
edition? (mf/deref edition-ref)
|
||||
|
||||
local-position-data (mf/use-state nil)
|
||||
|
||||
sid-ref (mf/use-ref nil)
|
||||
|
||||
handle-change-foreign-object
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when-let [position-data (utp/calc-position-data node)]
|
||||
(let [parent (dom/get-parent node)
|
||||
parent-transform (dom/get-attribute parent "transform")
|
||||
node-transform (dom/get-attribute node "transform")
|
||||
|
||||
parent-mtx (usvg/parse-transform parent-transform)
|
||||
node-mtx (usvg/parse-transform node-transform)
|
||||
|
||||
;; We need to see what transformation is applied in the DOM to reverse it
|
||||
;; before calculating the position data
|
||||
mtx (-> (gmt/multiply parent-mtx node-mtx)
|
||||
(gmt/inverse))
|
||||
|
||||
position-data
|
||||
(->> position-data
|
||||
(mapv #(merge % (-> (select-keys % [:x :y :width :height])
|
||||
(gsh/transform-rect mtx)))))]
|
||||
(reset! local-position-data position-data)))))
|
||||
|
||||
[node-ref on-change-node] (use-mutable-observer handle-change-foreign-object)
|
||||
|
||||
show-svg-text? (or (some? position-data) (some? @local-position-data))
|
||||
|
||||
shape
|
||||
(cond-> shape
|
||||
(some? @local-position-data)
|
||||
(assoc :position-data @local-position-data))
|
||||
|
||||
update-position-data
|
||||
(fn []
|
||||
(when (some? @local-position-data)
|
||||
(reset! local-position-data nil)
|
||||
(st/emit! (dch/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(assoc :position-data @local-position-data)))
|
||||
{:save-undo? false}))))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps @local-position-data)
|
||||
(fn []
|
||||
;; Timer to update the shape. We do this so a lot of changes won't produce
|
||||
;; a lot of updates (kind of a debounce)
|
||||
(let [sid (timers/schedule 50 update-position-data)]
|
||||
(fn []
|
||||
(rx/dispose! sid)))))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps show-svg-text?)
|
||||
(fn []
|
||||
(when-not show-svg-text?
|
||||
;; There is no position data we need to calculate it even if no change has happened
|
||||
;; this usualy happens the first time a text is rendered
|
||||
(let [update-data
|
||||
(fn update-data []
|
||||
(let [node (mf/ref-val node-ref)]
|
||||
(if (some? node)
|
||||
(let [position-data (utp/calc-position-data node)]
|
||||
(reset! local-position-data position-data))
|
||||
|
||||
;; No node present, we need to keep waiting
|
||||
(do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid))
|
||||
(when-not @local-position-data
|
||||
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))]
|
||||
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data))))
|
||||
|
||||
(fn []
|
||||
(when-let [sid (mf/ref-val sid-ref)]
|
||||
(rx/dispose! sid)))))
|
||||
|
||||
(let [shape (unchecked-get props "shape")]
|
||||
[:> shape-container {:shape shape}
|
||||
;; We keep hidden the shape when we're editing so it keeps track of the size
|
||||
;; and updates the selrect accordingly
|
||||
[:*
|
||||
[:g.text-shape {:ref on-change-node
|
||||
:opacity (when show-svg-text? 0)
|
||||
:pointer-events "none"}
|
||||
[:& text/text-shape {:shape shape}]
|
||||
|
||||
;; The `:key` prop here is mandatory because the
|
||||
;; text-resize-content breaks a hooks rule and we can't reuse
|
||||
;; the component if the edition flag changes.
|
||||
[:& text-resize-content {:shape
|
||||
(cond-> shape
|
||||
show-svg-text?
|
||||
(dissoc :transform :transform-inverse))
|
||||
:edition? edition?
|
||||
:key (str id edition?)}]]
|
||||
|
||||
(when show-svg-text?
|
||||
[:g.text-svg {:pointer-events "none"}
|
||||
[:& svg/text-shape {:shape shape}]])
|
||||
|
||||
(when (debug? :text-outline)
|
||||
(when (and (debug? :text-outline) (d/not-empty? (:position-data shape)))
|
||||
(for [data (:position-data shape)]
|
||||
(let [{:keys [x y width height]} data]
|
||||
[:*
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
;; 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) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.text.viewport-texts
|
||||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.text.fo-text :as fo]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as utp]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- update-with-editor-state
|
||||
"Updates the shape with the current state in the editor"
|
||||
[shape editor-state]
|
||||
(let [content (:content shape)
|
||||
editor-content
|
||||
(when editor-state
|
||||
(-> editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content)))]
|
||||
|
||||
(cond-> shape
|
||||
(some? editor-content)
|
||||
(assoc :content (attrs/merge content editor-content)))))
|
||||
|
||||
(mf/defc text-container
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
|
||||
handle-node-rendered
|
||||
(fn [node]
|
||||
(when node
|
||||
;; Check if we need to update the size because it's auto-width or auto-height
|
||||
(when (contains? #{:auto-height :auto-width} (:grow-type shape))
|
||||
(let [{:keys [width height]}
|
||||
(-> (dom/query node ".paragraph-set")
|
||||
(dom/get-client-size))]
|
||||
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(st/emit! (dwt/resize-text (:id shape) (mth/ceil width) (mth/ceil height))))))
|
||||
|
||||
;; Update the position-data of every text fragment
|
||||
(let [position-data (utp/calc-position-data node)]
|
||||
(st/emit! (dch/update-shapes
|
||||
[(:id shape)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(assoc :position-data position-data)))
|
||||
{:save-undo? false})))))]
|
||||
|
||||
[:& fo/text-shape {:key (str "shape-" (:id shape))
|
||||
:ref handle-node-rendered
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)}]))
|
||||
|
||||
(mf/defc viewport-texts
|
||||
[{:keys [objects edition]}]
|
||||
|
||||
(let [editor-state (-> (mf/deref refs/workspace-editor-state)
|
||||
(get edition))
|
||||
|
||||
text-shapes-ids
|
||||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(->> objects (vals) (filter cph/text-shape?) (map :id)))
|
||||
|
||||
text-shapes
|
||||
(mf/use-memo
|
||||
(mf/deps text-shapes-ids editor-state edition)
|
||||
#(cond-> (select-keys objects text-shapes-ids)
|
||||
(some? editor-state)
|
||||
(d/update-when edition update-with-editor-state editor-state)))
|
||||
|
||||
prev-text-shapes (hooks/use-previous text-shapes)
|
||||
|
||||
;; A change in position-data won't be a "real" change
|
||||
text-change?
|
||||
(fn [id]
|
||||
(not= (-> (get text-shapes id)
|
||||
(dissoc :position-data))
|
||||
(-> (get prev-text-shapes id)
|
||||
(dissoc :position-data))))
|
||||
|
||||
changed-texts
|
||||
(->> (keys text-shapes)
|
||||
(filter text-change?)
|
||||
(map (d/getf text-shapes)))]
|
||||
|
||||
(for [{:keys [id] :as shape} changed-texts]
|
||||
[:& text-container {:shape (dissoc shape :transform :transform-inverse)
|
||||
:key (str (dm/str "text-container-" id))}])))
|
|
@ -275,21 +275,21 @@
|
|||
:key id}])))]]))
|
||||
|
||||
(defn- strip-obj-data [obj]
|
||||
(select-keys obj [:id
|
||||
:name
|
||||
:blocked
|
||||
:hidden
|
||||
:shapes
|
||||
:type
|
||||
:content
|
||||
:parent-id
|
||||
:component-id
|
||||
:component-file
|
||||
:shape-ref
|
||||
:touched
|
||||
:metadata
|
||||
:masked-group?
|
||||
:bool-type]))
|
||||
(dm/select-keys obj [:id
|
||||
:name
|
||||
:blocked
|
||||
:hidden
|
||||
:shapes
|
||||
:type
|
||||
:content
|
||||
:parent-id
|
||||
:component-id
|
||||
:component-file
|
||||
:shape-ref
|
||||
:touched
|
||||
:metadata
|
||||
:masked-group?
|
||||
:bool-type]))
|
||||
|
||||
(defn- strip-objects
|
||||
"Remove unnecesary data from objects map"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.context :as ctx]
|
||||
|
@ -17,6 +18,7 @@
|
|||
[app.main.ui.shapes.export :as use]
|
||||
[app.main.ui.workspace.shapes :as shapes]
|
||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
||||
[app.main.ui.workspace.shapes.text.viewport-texts :as stv]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.comments :as comments]
|
||||
[app.main.ui.workspace.viewport.drawarea :as drawarea]
|
||||
|
@ -33,7 +35,6 @@
|
|||
[app.main.ui.workspace.viewport.selection :as selection]
|
||||
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
|
||||
[app.main.ui.workspace.viewport.snap-points :as snap-points]
|
||||
[app.main.ui.workspace.viewport.thumbnail-renderer :as wtr]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||
[beicon.core :as rx]
|
||||
|
@ -67,9 +68,13 @@
|
|||
drawing (mf/deref refs/workspace-drawing)
|
||||
options (mf/deref refs/workspace-page-options)
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
base-objects (-> (mf/deref refs/workspace-page-objects)
|
||||
|
||||
objects-ref (mf/use-memo #(refs/workspace-page-objects-by-id page-id))
|
||||
base-objects (-> (mf/deref objects-ref)
|
||||
(ui-hooks/with-focus-objects focus))
|
||||
|
||||
modifiers (mf/deref refs/workspace-modifiers)
|
||||
|
||||
objects-modified (mf/with-memo [base-objects modifiers]
|
||||
(gsh/merge-modifiers base-objects modifiers))
|
||||
|
||||
|
@ -176,15 +181,12 @@
|
|||
(hooks/setup-keyboard alt? mod? space?)
|
||||
(hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom)
|
||||
(hooks/setup-viewport-modifiers modifiers base-objects)
|
||||
|
||||
(hooks/setup-shortcuts node-editing? drawing-path?)
|
||||
(hooks/setup-active-frames base-objects vbox hover active-frames)
|
||||
(hooks/setup-active-frames base-objects vbox hover active-frames zoom)
|
||||
|
||||
[:div.viewport
|
||||
[:div.viewport-overlays {:ref overlays-ref}
|
||||
|
||||
[:& wtr/frame-renderer {:objects base-objects
|
||||
:background background}]
|
||||
|
||||
(when show-text-editor?
|
||||
[:& editor/text-editor-viewport {:shape editing-shape
|
||||
:viewport-ref viewport-ref
|
||||
|
@ -230,6 +232,22 @@
|
|||
:objects base-objects
|
||||
:active-frames @active-frames}]]]]
|
||||
|
||||
[:svg.render-shapes
|
||||
{:id "text-position-layer"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:key (str "text-position-layer" page-id)
|
||||
:width (:width vport 0)
|
||||
:height (:height vport 0)
|
||||
:view-box (utils/format-viewbox vbox)}
|
||||
|
||||
[:g {:pointer-events "none" :opacity 0}
|
||||
[:& stv/viewport-texts {:key (dm/str "texts-" page-id)
|
||||
:page-id page-id
|
||||
:objects base-objects
|
||||
:edition edition}]]]
|
||||
|
||||
[:svg.viewport-controls
|
||||
{:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
|
@ -225,15 +226,15 @@
|
|||
(fn []
|
||||
(when (and (nil? @prev-transforms)
|
||||
(some? transforms))
|
||||
(utils/start-transform! shapes))
|
||||
(utils/start-transform! globals/document shapes))
|
||||
|
||||
(when (some? modifiers)
|
||||
(utils/update-transform! shapes transforms modifiers))
|
||||
(utils/update-transform! globals/document shapes transforms modifiers))
|
||||
|
||||
|
||||
(when (and (some? @prev-modifiers)
|
||||
(not (some? modifiers)))
|
||||
(utils/remove-transform! @prev-shapes))
|
||||
(utils/remove-transform! globals/document @prev-shapes))
|
||||
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
|
@ -246,7 +247,7 @@
|
|||
(gsh/overlaps? frame vbox))))
|
||||
|
||||
(defn setup-active-frames
|
||||
[objects vbox hover active-frames]
|
||||
[objects vbox hover active-frames zoom]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps vbox)
|
||||
|
@ -262,13 +263,16 @@
|
|||
(reduce-kv set-active-frames {} active-frames))))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @hover @active-frames)
|
||||
(mf/deps @hover @active-frames zoom)
|
||||
(fn []
|
||||
(let [frame-id (if (= :frame (:type @hover))
|
||||
(:id @hover)
|
||||
(:frame-id @hover))]
|
||||
(when (not (contains? @active-frames frame-id))
|
||||
(swap! active-frames assoc frame-id true))))))
|
||||
(if (< zoom 0.25)
|
||||
(when (some? @active-frames)
|
||||
(reset! active-frames nil))
|
||||
(when (and (some? frame-id)(not (contains? @active-frames frame-id)))
|
||||
(reset! active-frames {frame-id true})))))))
|
||||
|
||||
;; NOTE: this is executed on each page change, maybe we need to move
|
||||
;; this shortcuts outside the viewport?
|
||||
|
|
|
@ -77,8 +77,8 @@
|
|||
|
||||
(defn get-nodes
|
||||
"Retrieve the DOM nodes to apply the matrix transformation"
|
||||
[{:keys [id type masked-group?]}]
|
||||
(let [shape-node (dom/get-element (str "shape-" id))
|
||||
[base-node {:keys [id type masked-group?]}]
|
||||
(let [shape-node (dom/query base-node (str "#shape-" id))
|
||||
|
||||
frame? (= :frame type)
|
||||
group? (= :group type)
|
||||
|
@ -86,7 +86,7 @@
|
|||
mask? (and group? masked-group?)
|
||||
|
||||
;; When the shape is a frame we maybe need to move its thumbnail
|
||||
thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))]
|
||||
thumb-node (when frame? (dom/query base-node (str "#thumbnail-" id)))]
|
||||
|
||||
(cond
|
||||
frame?
|
||||
|
@ -132,9 +132,9 @@
|
|||
(dom/set-attribute! node "height" height)))
|
||||
|
||||
(defn start-transform!
|
||||
[shapes]
|
||||
[base-node shapes]
|
||||
(doseq [shape shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(doseq [node nodes]
|
||||
(let [old-transform (dom/get-attribute node "transform")]
|
||||
(when (some? old-transform)
|
||||
|
@ -168,9 +168,9 @@
|
|||
(dom/set-attribute! node att (str new-value))))
|
||||
|
||||
(defn update-transform!
|
||||
[shapes transforms modifiers]
|
||||
[base-node shapes transforms modifiers]
|
||||
(doseq [{:keys [id type] :as shape} shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(let [transform (get transforms id)
|
||||
modifiers (get-in modifiers [id :modifiers])
|
||||
|
||||
|
@ -214,9 +214,9 @@
|
|||
(set-transform-att! node "transform" transform)))))))
|
||||
|
||||
(defn remove-transform!
|
||||
[shapes]
|
||||
[base-node shapes]
|
||||
(doseq [shape shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(doseq [node nodes]
|
||||
(when (some? node)
|
||||
(cond
|
||||
|
|
Loading…
Add table
Reference in a new issue