diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index accbc9e13..97a800449 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -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] diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index e2762f954..54074465d 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index cc30870c9..d9c2b72ba 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -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))) + + diff --git a/frontend/src/app/main/ui/hooks/mutable_observer.cljs b/frontend/src/app/main/ui/hooks/mutable_observer.cljs index b3dabaf45..239deea2e 100644 --- a/frontend/src/app/main/ui/hooks/mutable_observer.cljs +++ b/frontend/src/app/main/ui/hooks/mutable_observer.cljs @@ -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 [] diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 1e2d85df5..5208555d7 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -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"})) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 68c26f713..b2b27b483 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -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 diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index 9e2730138..1b70034c0 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -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}]))) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index adff4aa4c..adcef52ad 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 0c113cb3d..8142c059b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -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?}]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index 18c1237ae..0e7c38883 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 85e635161..270822968 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -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] [:* diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs new file mode 100644 index 000000000..dd9761822 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -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))}]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index dd20e2dd9..3bcef6b86 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -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" diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a4da7e61d..94313a3b2 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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" diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index d2e70e878..d0eae1ddf 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -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? diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index f4151f11f..8d88e5c27 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -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