0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Performance improvements

This commit is contained in:
alonso.torres 2022-04-12 16:49:52 +02:00
parent 814042909a
commit b576ef02af
16 changed files with 510 additions and 315 deletions

View file

@ -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]

View file

@ -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}]

View file

@ -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)))

View file

@ -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 []

View file

@ -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"}))

View file

@ -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

View file

@ -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}])))

View file

@ -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)

View file

@ -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?}]]))))

View file

@ -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

View file

@ -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]
[:*

View file

@ -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))}])))

View file

@ -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"

View file

@ -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"

View file

@ -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?

View file

@ -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