diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index afc1bb6bb9..e201d3181a 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -133,6 +133,7 @@ (dm/export gtr/transform-bounds) (dm/export gtr/modifiers->transform) (dm/export gtr/empty-modifiers?) +(dm/export gtr/move-position-data) ;; Constratins (dm/export gct/calc-child-modifiers) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 943a7f2fca..42a4c76ac0 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -139,6 +139,18 @@ (:shapes) (keep lookup))))) +(defn get-frames-ids + "Retrieves all frame objects as vector. It is not implemented in + function of `get-immediate-children` for performance reasons. This + function is executed in the render hot path." + [objects] + (let [lookup (d/getf objects) + xform (comp (keep lookup) + (filter frame-shape?) + (map :id))] + (->> (:shapes (lookup uuid/zero)) + (into [] xform)))) + (defn get-frames "Retrieves all frame objects as vector. It is not implemented in function of `get-immediate-children` for performance reasons. This @@ -474,3 +486,19 @@ [objects frame-id] (let [ids (concat [frame-id] (get-children-ids objects frame-id))] (select-keys objects ids))) + +(defn objects-by-frame + "Returns a map of the `objects` grouped by frame. Every value of the map has + the same format as objects id->shape-data" + [objects] + ;; Implemented with transients for performance. 30~50% better + (letfn [(process-shape [objects [id shape]] + (let [frame-id (if (= :frame (:type shape)) id (:frame-id shape)) + cur (-> (or (get objects frame-id) (transient {})) + (assoc! id shape))] + (assoc! objects frame-id cur)))] + (d/update-vals + (->> objects + (reduce process-shape (transient {})) + (persistent!)) + persistent!))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 7a1ccbcb0e..930b7fc999 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -8,6 +8,7 @@ (:require [app.common.attrs :as attrs] [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] @@ -303,83 +304,27 @@ (defn not-changed? [old-dim new-dim] (> (mth/abs (- old-dim new-dim)) 0.1)) -(defn resize-text-batch [changes] - (ptk/reify ::resize-text-batch - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects])] - (if-not (every? #(contains? objects(first %)) changes) - (rx/empty) - - (let [changes-map (->> changes (into {})) - ids (keys changes-map) - update-fn - (fn [shape] - (let [[new-width new-height] (get changes-map (:id shape)) - {:keys [selrect grow-type]} (gsh/transform-shape shape) - {shape-width :width shape-height :height} selrect - - modifier-width (gsh/resize-modifiers shape :width new-width) - modifier-height (gsh/resize-modifiers shape :height new-height)] - - (cond-> shape - (and (not-changed? shape-width new-width) (= grow-type :auto-width)) - (-> (assoc :modifiers modifier-width) - (gsh/transform-shape)) - - (and (not-changed? shape-height new-height) - (or (= grow-type :auto-height) (= grow-type :auto-width))) - (-> (assoc :modifiers modifier-height) - (gsh/transform-shape)))))] - - (rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))) - -;; When a resize-event arrives we start "buffering" for a time -;; after that time we invoke `resize-text-batch` with all the changes -;; together. This improves the performance because we only re-render the -;; resized components once even if there are changes that applies to -;; lots of texts like changing a font (defn resize-text [id new-width new-height] (ptk/reify ::resize-text - IDeref - (-deref [_] - {:id id :width new-width :height new-height}) - ptk/WatchEvent - (watch [_ state stream] - (let [;; This stream aggregates the events of "resizing" - resize-events - (rx/merge - (->> (rx/of (resize-text id new-width new-height))) - (->> stream (rx/filter (ptk/type? ::resize-text)))) + (watch [_ _ _] + (letfn [(update-fn [shape] + (let [{:keys [selrect grow-type]} shape + {shape-width :width shape-height :height} selrect + modifier-width (gsh/resize-modifiers shape :width new-width) + modifier-height (gsh/resize-modifiers shape :height new-height)] + (cond-> shape + (and (not-changed? shape-width new-width) (= grow-type :auto-width)) + (-> (assoc :modifiers modifier-width) + (gsh/transform-shape)) - ;; Stop buffering after time without resizes - stop-buffer (->> resize-events (rx/debounce 100)) + (and (not-changed? shape-height new-height) + (or (= grow-type :auto-height) (= grow-type :auto-width))) + (-> (assoc :modifiers modifier-height) + (gsh/transform-shape)))))] - ;; Aggregates the resizes so only send the resize when the sizes are stable - resize-batch - (->> resize-events - (rx/take-until stop-buffer) - (rx/reduce (fn [acc event] - (assoc acc (:id @event) [(:width @event) (:height @event)])) - {id [new-width new-height]}) - (rx/map #(resize-text-batch %))) - - ;; This stream retrieves the changes of page so we cancel the agregation - change-page - (->> stream - (rx/filter (ptk/type? :app.main.data.workspace/finalize-page)) - (rx/take 1) - (rx/ignore))] - - (if-not (::handling-texts state) - (->> (rx/concat - (rx/of #(assoc % ::handling-texts true)) - (rx/race resize-batch change-page) - (rx/of #(dissoc % ::handling-texts)))) - (rx/empty)))))) + (rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false})))))) (defn save-font [data] @@ -391,3 +336,46 @@ (not multiple?) (assoc-in [:workspace-global :default-font] data)))))) +(defn apply-text-modifier + [shape {:keys [width height position-data]}] + + (let [modifier-width (when width (gsh/resize-modifiers shape :width width)) + modifier-height (when height (gsh/resize-modifiers shape :height height)) + + new-shape + (cond-> shape + (some? modifier-width) + (-> (assoc :modifiers modifier-width) + (gsh/transform-shape)) + + (some? modifier-height) + (-> (assoc :modifiers modifier-height) + (gsh/transform-shape)) + + (some? position-data) + (assoc :position-data position-data)) + + delta-move + (gpt/subtract (gpt/point (:selrect new-shape)) + (gpt/point (:selrect shape))) + + + new-shape + (update new-shape :position-data gsh/move-position-data (:x delta-move) (:y delta-move))] + + + new-shape)) + +(defn update-text-modifier + [id props] + (ptk/reify ::update-text-modifier + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-text-modifier id] (fnil merge {}) props)))) + +(defn remove-text-modifier + [id] + (ptk/reify ::remove-text-modifier + ptk/UpdateEvent + (update [_ state] + (d/dissoc-in state [:workspace-text-modifier id])))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 4c12f464f8..2d5514c2eb 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -401,3 +401,9 @@ (defn thumbnail-frame-data [frame-id] (l/derived #(get % frame-id) thumbnail-data)) + +(def workspace-text-modifier + (l/derived :workspace-text-modifier st/state)) + +(defn workspace-text-modifier-by-id [id] + (l/derived #(get % id) workspace-text-modifier)) diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 2f3181c0ef..f879e2345d 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -220,8 +220,9 @@ (mf/defc selection-guides [{:keys [bounds selrect zoom]}] [:g.selection-guides - (for [[x1 y1 x2 y2] (calculate-guides bounds selrect)] - [:line {:x1 x1 + (for [[idx [x1 y1 x2 y2]] (d/enumerate (calculate-guides bounds selrect))] + [:line {:key (dm/str "guide-" idx) + :x1 x1 :y1 y1 :x2 x2 :y2 y2 diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 9debee8822..fa69ae097f 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -423,8 +423,9 @@ shape (obj/get props "shape") elem-name (obj/get child "type") render-id (mf/use-ctx muc/render-ctx) + stroke-id (dm/fmt "strokes-%" (:id shape)) stroke-props (-> (obj/new) - (obj/set! "id" (dm/fmt "strokes-%" (:id shape))) + (obj/set! "id" stroke-id) (cond-> ;; There is a blur (and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not)) @@ -440,7 +441,7 @@ (for [[index value] (-> (d/enumerate (:strokes shape)) reverse)] (let [props (build-stroke-props index child value render-id) shape (assoc value :points (:points shape))] - [:& shape-custom-stroke {:shape shape :index index} + [:& shape-custom-stroke {:shape shape :index index :key (dm/str index "-" stroke-id)} [:> elem-name props]]))])])) (mf/defc shape-custom-strokes diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 86ef063ed1..1e6a259d19 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -20,9 +20,13 @@ (mf/defc render-text {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - text (:text node) - style (sts/generate-text-styles node)] + (let [node (obj/get props "node") + parent (obj/get props "parent") + shape (obj/get props "shape") + text (:text node) + style (if (= text "") + (sts/generate-text-styles shape parent) + (sts/generate-text-styles shape node))] [:span.text-node {:style style} (if (= text "") "\u00A0" text)])) @@ -60,7 +64,7 @@ (mf/defc render-node {::mf/wrap-props false} [props] - (let [{:keys [type text children] :as node} (obj/get props "node")] + (let [{:keys [type text children]} (obj/get props "node")] (if (string? text) [:> render-text props] (let [component (case type diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 57ded6ac2f..cd2a21c5b9 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -15,9 +15,8 @@ [cuerdas.core :as str])) (defn generate-root-styles - [shape node] + [{:keys [width height]} node] (let [valign (:vertical-align node "top") - {:keys [width height]} shape base #js {:height height :width width :fontFamily "sourcesanspro" @@ -57,10 +56,10 @@ (some? text-align) (obj/set! "textAlign" text-align)))) (defn generate-text-styles - ([data] - (generate-text-styles data nil)) + ([shape data] + (generate-text-styles shape data nil)) - ([data {:keys [show-text?] :or {show-text? true}}] + ([{:keys [grow-type] :as shape} data {:keys [show-text?] :or {show-text? true}}] (let [letter-spacing (:letter-spacing data 0) text-decoration (:text-decoration data) text-transform (:text-transform data) @@ -81,7 +80,7 @@ base #js {:textDecoration text-decoration :textTransform text-transform - :lineHeight (or line-height "inherit") + :lineHeight (or line-height "1.2") :color (if show-text? text-color "transparent") :caretColor (or text-color "black") :overflowWrap "initial"} @@ -99,33 +98,35 @@ (nil? (:fills data)) [{:fill-color "#000000" :fill-opacity 1}]) - base (cond-> base - (some? fills) - (obj/set! "--fills" (transit/encode-str fills)))] - (when (and (string? letter-spacing) - (pos? (alength letter-spacing))) - (obj/set! base "letterSpacing" (str letter-spacing "px"))) + font (when (and (string? font-id) (pos? (alength font-id))) + (get fontsdb font-id)) - (when (and (string? font-size) - (pos? (alength font-size))) - (obj/set! base "fontSize" (str font-size "px"))) + [font-family font-style font-weight] + (when (some? font) + (fonts/ensure-loaded! font-id) + (let [font-variant (d/seek #(= font-variant-id (:id %)) (:variants font))] + [(str/quote (or (:family font) (:font-family data))) + (or (:style font-variant) (:font-style data)) + (or (:weight font-variant) (:font-weight data))]))] - (when (and (string? font-id) - (pos? (alength font-id))) - (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id) - font-family (str/quote - (or (:family font) - (:font-family data))) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (:font-style data)) - font-weight (or (:weight font-variant) - (:font-weight data))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight))) + (cond-> base + (some? fills) + (obj/set! "--fills" (transit/encode-str fills)) - base))) + (and (string? letter-spacing) (pos? (alength letter-spacing))) + (obj/set! "letterSpacing" (str letter-spacing "px")) + + (and (string? font-size) (pos? (alength font-size))) + (obj/set! "fontSize" (str font-size "px")) + + (some? font) + (-> (obj/set! "fontFamily" font-family) + (obj/set! "fontStyle" font-style) + (obj/set! "fontWeight" font-weight)) + + (= grow-type :auto-width) + (obj/set! "whiteSpace" "pre") + + (not= grow-type :auto-width) + (obj/set! "whiteSpace" "pre-wrap"))))) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index 7ebf916307..05c259503c 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.text.svg-text (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.config :as cfg] [app.main.ui.context :as muc] @@ -57,7 +58,8 @@ alignment-bl (when (cfg/check-browser? :safari) "text-before-edge") dominant-bl (when-not (cfg/check-browser? :safari) "ideographic") - props (-> #js {:x (:x data) + props (-> #js {:key (dm/str "text-" (:id shape) "-" index) + :x (:x data) :y y :alignmentBaseline alignment-bl :dominantBaseline dominant-bl diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index adcef52ad8..78cb6ab8b8 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -44,7 +44,14 @@ [props] (let [objects (obj/get props "objects") active-frames (obj/get props "active-frames") - shapes (cph/get-immediate-children objects)] + shapes (cph/get-immediate-children objects) + + ;; We group the objects together per frame-id so if an object of a different + ;; frame changes won't affect the rendering frame + frame-objects + (mf/use-memo + (mf/deps objects) + #(cph/objects-by-frame objects))] [:* ;; Render font faces only for shapes that are part of the root ;; frame but don't belongs to any other frame. @@ -57,7 +64,7 @@ (if (cph/frame-shape? item) [:& frame-wrapper {:shape item :key (:id item) - :objects objects + :objects (get frame-objects (:id item)) :thumbnail? (not (get active-frames (:id item) false))}] [:& shape-wrapper {:shape item diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index fa7f9c06ad..a81c8b5d1c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.workspace.shapes.frame (:require [app.common.data :as d] - [app.common.pages.helpers :as cph] [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] [app.main.ui.hooks :as hooks] @@ -40,12 +39,19 @@ [:& ff/fontfaces-style {:fonts fonts}] [:> frame-shape {:shape shape :childs childs} ]]])))) +(defn check-props + [new-props old-props] + (and + (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) + (= (unchecked-get new-props "shape") (unchecked-get old-props "shape")) + (= (unchecked-get new-props "objects") (unchecked-get old-props "objects")))) + (defn frame-wrapper-factory [shape-wrapper] (let [frame-shape (frame-shape-factory shape-wrapper)] (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "thumbnail?" "objects"]))] + {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] @@ -53,16 +59,10 @@ thumbnail? (unchecked-get props "thumbnail?") objects (unchecked-get props "objects") - objects (mf/use-memo - (mf/deps objects) - #(cph/get-frame-objects objects (:id shape))) - - objects (hooks/use-equal-memo objects) - fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects)) fonts (-> fonts (hooks/use-equal-memo)) - force-render (mf/use-state false) + force-render (mf/use-state false) ;; Thumbnail data frame-id (:id shape) @@ -76,23 +76,30 @@ ;; when `true` we've called the mount for the frame rendered? (mf/use-var false) - modifiers (fdm/use-dynamic-modifiers shape objects node-ref) + ;; Modifiers + modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers-ref) - disable? (d/not-empty? (get-in modifiers [(:id shape) :modifiers])) + disable-thumbnail? (d/not-empty? (get-in modifiers [(:id shape) :modifiers])) [on-load-frame-dom thumb-renderer] - (ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable?) + (ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable-thumbnail?) on-frame-load (fns/use-node-store thumbnail? node-ref rendered?)] + (fdm/use-dynamic-modifiers objects @node-ref modifiers) + (mf/use-effect (fn [] ;; When a change in the data is received a "force-render" event is emited ;; that will force the component to be mounted in memory - (->> (dwt/force-render-stream (:id shape)) - (rx/take-while #(not @rendered?)) - (rx/subs #(reset! force-render true))))) + (let [sub + (->> (dwt/force-render-stream (:id shape)) + (rx/take-while #(not @rendered?)) + (rx/subs #(reset! force-render true)))] + #(when sub + (rx/dispose! sub))))) (mf/use-effect (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render) @@ -105,8 +112,7 @@ @node-ref) (when (not @rendered?) (reset! rendered? true))))) - [:g.frame-container {:key "frame-container" - :ref on-frame-load} + [:g.frame-container {:key "frame-container" :ref on-frame-load} thumb-renderer [:g.frame-thumbnail diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 1306b6cbb1..2fde514911 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -8,21 +8,13 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.main.refs :as refs] [app.main.ui.workspace.viewport.utils :as utils] [rumext.alpha :as mf])) (defn use-dynamic-modifiers - [shape objects node-ref] + [objects node modifiers] - (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 + (let [transforms (mf/use-memo (mf/deps modifiers) (fn [] @@ -48,16 +40,15 @@ (fn [] (when (and (nil? @prev-transforms) (some? transforms)) - (utils/start-transform! @node-ref shapes)) + (utils/start-transform! node shapes)) (when (some? modifiers) - (utils/update-transform! @node-ref shapes transforms modifiers)) + (utils/update-transform! node shapes transforms modifiers)) (when (and (some? @prev-modifiers) (empty? modifiers)) - (utils/remove-transform! @node-ref @prev-shapes)) + (utils/remove-transform! node @prev-shapes)) (reset! prev-modifiers modifiers) (reset! prev-transforms transforms) - (reset! prev-shapes shapes))) - modifiers)) + (reset! prev-shapes shapes))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs index f16584ca64..aea3696a0c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -23,7 +23,7 @@ canvas-height (.-height canvas-node)] (.clearRect canvas-context 0 0 canvas-width canvas-height) (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - (.toDataURL canvas-node "image/webp" 0.75))) + (.toDataURL canvas-node "image/jpeg" 0.8))) (defn use-render-thumbnail "Hook that will create the thumbnail thata" @@ -48,10 +48,11 @@ (mf/use-callback (fn [] (let [canvas-node (mf/ref-val frame-canvas-ref) - img-node (mf/ref-val frame-image-ref) - thumb-data (draw-thumbnail-canvas canvas-node img-node)] - (st/emit! (dw/update-thumbnail id thumb-data)) - (reset! image-url nil)))) + img-node (mf/ref-val frame-image-ref)] + (ts/raf + #(let [thumb-data (draw-thumbnail-canvas canvas-node img-node)] + (st/emit! (dw/update-thumbnail id thumb-data)) + (reset! image-url nil)))))) on-change (mf/use-callback @@ -67,6 +68,7 @@ (dom/set-property! "viewBox" (dm/str x " " y " " width " " height)) (dom/set-property! "width" width) (dom/set-property! "height" height) + (dom/set-property! "fill" "none") (obj/set! "innerHTML" frame-html)) img-src (-> svg-node dom/node->xml dom/svg->data-uri)] diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 2708229686..821fca5365 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -8,6 +8,8 @@ (:require [app.common.data :as d] [app.common.math :as mth] + [app.main.data.workspace.texts :as dwt] + [app.main.refs :as refs] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text :as text] [debug :refer [debug?]] @@ -17,10 +19,22 @@ (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape")] + (let [shape (unchecked-get props "shape") + + text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (mf/deref text-modifier-ref) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier))] + [:> shape-container {:shape shape} [:* - [:& text/text-shape {:shape shape}] + [:g.text-shape + [:& text/text-shape {:shape shape}]] (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) (for [data (:position-data shape)] diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 4918d52acc..44ff09f651 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.text :as txt] @@ -57,14 +58,13 @@ :shape shape}} nil))) -(defn styles-fn [styles content] - (if (= (.getText content) "") - (-> (.getData content) - (.toJS) - (js->clj :keywordize-keys true) - (sts/generate-text-styles {:show-text? false})) - (-> (txt/styles-to-attrs styles) - (sts/generate-text-styles {:show-text? false})))) +(defn styles-fn [shape styles content] + (let [data (if (= (.getText content) "") + (-> (.getData content) + (.toJS) + (js->clj :keywordize-keys true)) + (txt/styles-to-attrs styles))] + (sts/generate-text-styles shape data {:show-text? false}))) (def default-decorator (ted/create-decorator "PENPOT_SELECTION" selection-component)) @@ -96,6 +96,16 @@ state (get state-map id empty-editor-state) self-ref (mf/use-ref) + text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (mf/deref text-modifier-ref) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier)) + blurred (mf/use-var false) on-key-up @@ -227,7 +237,7 @@ :handle-return handle-return :strip-pasted-styles true :handle-pasted-text handle-pasted-text - :custom-style-fn styles-fn + :custom-style-fn (partial styles-fn shape) :block-renderer-fn #(render-block % shape) :ref on-editor :editor-state state}]])) @@ -252,15 +262,20 @@ position (-> (gpt/point (-> shape :selrect :x) (-> shape :selrect :y)) - (translate-point-from-viewport (mf/ref-val viewport-ref) zoom))] + (translate-point-from-viewport (mf/ref-val viewport-ref) zoom)) + + top-left-corner (gpt/point (/ (:width shape) 2) (/ (:height shape) 2)) + + transform + (-> (gmt/matrix) + (gmt/scale (gpt/point zoom)) + (gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))] [:div {:style {:position "absolute" :left (str (:x position) "px") :top (str (:y position) "px") :pointer-events "all" - :transform (str (gsh/transform-matrix shape nil (gpt/point 0 0))) - :transform-origin "center center"}} + :transform (str transform) + :transform-origin "left top"}} - [:div {:style {:transform (str "scale(" zoom ")") - :transform-origin "top left"}} - [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]])) + [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs new file mode 100644 index 0000000000..ba2bfa059e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs @@ -0,0 +1,37 @@ +;; 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.text-edition-outline + (:require + [app.common.geom.shapes :as gsh] + [app.main.data.workspace.texts :as dwt] + [app.main.refs :as refs] + [rumext.alpha :as mf])) + +(mf/defc text-edition-outline + [{:keys [shape zoom]}] + (let [text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (mf/deref text-modifier-ref) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier)) + + transform (gsh/transform-matrix shape {:no-flip true}) + {:keys [x y width height]} shape] + + [:rect.main.viewport-selrect + {:x x + :y y + :width width + :height height + :transform (str transform) + :style {:stroke "var(--color-select)" + :stroke-width (/ 1 zoom) + :fill "none"}}])) 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 index 440188c1b1..17e1aaae63 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -11,8 +11,10 @@ [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.pages.helpers :as cph] + [app.common.text :as txt] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.texts :as dwt] + [app.main.fonts :as fonts] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.hooks :as hooks] @@ -35,76 +37,171 @@ (ted/export-content)))] (cond-> shape - (some? editor-content) + (and (some? shape) (some? editor-content)) (assoc :content (attrs/merge content editor-content))))) +(defn- update-text-shape + [{:keys [grow-type id]} 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) + (let [{:keys [width height]} + (-> (dom/query node ".paragraph-set") + (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (st/emit! (dwt/resize-text id width height))))) + + ;; Update the position-data of every text fragment + (let [position-data (utp/calc-position-data node)] + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (assoc :position-data position-data))) + {:save-undo? false})))) + +(defn- update-text-modifier + [{:keys [grow-type id]} node] + + (let [position-data (utp/calc-position-data node) + props {:position-data position-data} + + props + (if (contains? #{:auto-height :auto-width} grow-type) + (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (assoc props :width width :height height) + props)) + props)] + + (st/emit! (dwt/update-text-modifier id props)))) + (mf/defc text-container - {::mf/wrap-props false - ::mf/wrap [mf/memo - #(mf/deferred % ts/idle-then-raf)]} + {::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") + (let [shape (obj/get props "shape") + on-update (obj/get props "on-update") + watch-edits (obj/get props "watch-edits") - 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)))))) + handle-update + (mf/use-callback + (mf/deps shape on-update) + (fn [node] + (when (some? node) + (on-update shape node)))) - ;; 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})))))] + text-modifier-ref + (mf/use-memo + (mf/deps (:id shape)) + #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (when watch-edits (mf/deref text-modifier-ref)) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier))] [:& fo/text-shape {:key (str "shape-" (:id shape)) - :ref handle-node-rendered + :ref handle-update :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))) - +(mf/defc viewport-texts-wrapper + {::mf/wrap-props false + ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} + [props] + (let [text-shapes (obj/get props "text-shapes") 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)))) + (let [old-shape (get prev-text-shapes id) + new-shape (get text-shapes id)] + (and (not (identical? old-shape new-shape)) + (not= old-shape new-shape)))) changed-texts (mf/use-memo (mf/deps text-shapes) #(->> (keys text-shapes) (filter text-change?) - (map (d/getf text-shapes))))] + (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))}]))) + handle-update-shape (mf/use-callback update-text-shape)] + + [:* + (for [{:keys [id] :as shape} changed-texts] + [:& text-container {:shape shape + :on-update handle-update-shape + :key (str (dm/str "text-container-" id))}])])) + +(defn strip-position-data [[id shape]] + (let [shape (dissoc shape :position-data :transform :transform-inverse)] + [id shape])) + + +(mf/defc viewport-text-editing + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + + ;; Join current objects with the state of the editor + editor-state + (-> (mf/deref refs/workspace-editor-state) + (get (:id shape))) + + shape (cond-> shape + (some? editor-state) + (update-with-editor-state editor-state)) + + handle-update-shape (mf/use-callback update-text-modifier)] + + (mf/use-effect + (mf/deps (:id shape)) + (fn [] + #(st/emit! (dwt/remove-text-modifier (:id shape))))) + + [:& text-container {:shape shape + :watch-edits true + :on-update handle-update-shape}])) + +(defn check-props + [new-props old-props] + (and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects")) + (= (unchecked-get new-props "edition") (unchecked-get old-props "edition")))) + +(mf/defc viewport-texts + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % check-props)]} + [props] + (let [objects (obj/get props "objects") + edition (obj/get props "edition") + + xf-texts (comp (filter (comp cph/text-shape? second)) + (map strip-position-data)) + + text-shapes + (mf/use-memo + (mf/deps objects) + #(into {} xf-texts objects)) + + editing-shape (get text-shapes edition)] + + ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are + ;; edited + (mf/use-effect + (fn [] + (let [text-nodes (->> text-shapes (vals)(mapcat #(txt/node-seq txt/is-text-node? (:content %)))) + fonts (into #{} (keep :font-id) text-nodes)] + (run! fonts/ensure-loaded! fonts)))) + + [:* + (when editing-shape + [:& viewport-text-editing {:shape editing-shape}]) + [:& viewport-texts-wrapper {:text-shapes text-shapes}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 3bcef6b86a..084dfc441f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -245,7 +245,7 @@ (mf/defc frame-wrapper {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["selected" "item" "index" "objects"])) + ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [props] [:> layer-item props]) @@ -274,33 +274,6 @@ :objects objects :key id}])))]])) -(defn- strip-obj-data [obj] - (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" - [objects] - (persistent! - (->> objects - (reduce-kv - (fn [res id obj] - (assoc! res id (strip-obj-data obj))) - (transient {}))))) - (mf/defc layers-tree-wrapper {::mf/wrap-props false ::mf/wrap [mf/memo #(mf/throttle % 200)]} @@ -312,10 +285,8 @@ filters) objects (-> (obj/get props "objects") (hooks/use-equal-memo)) - objects (mf/use-memo - (mf/deps objects) - #(strip-objects objects)) + ;; TODO: Fix performance reparented-objects (d/mapm (fn [_ val] (assoc val :parent-id uuid/zero :shapes nil)) objects) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 876b7f2c1d..e18ad8832e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.hooks :as h] @@ -171,7 +172,8 @@ (seq (:strokes values)) [:& h/sortable-container {} (for [[index value] (d/enumerate (:strokes values []))] - [:& stroke-row {:stroke value + [:& stroke-row {:key (dm/str "stroke-" index) + :stroke value :title (tr "workspace.options.stroke-color") :index index :show-caps show-caps diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 2d4a4f6b5d..3967aa506a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -8,6 +8,7 @@ (:require ["react-virtualized" :as rvt] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.pages.helpers :as cph] [app.common.text :as txt] @@ -193,8 +194,8 @@ [:hr] [* [:p.title (tr "workspace.options.recent-fonts")] - (for [font recent-fonts] - [:& font-item {:key (:id font) + (for [[idx font] (d/enumerate recent-fonts)] + [:& font-item {:key (dm/str "font-" idx) :font font :style {} :on-click on-select-and-close diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 16941ad06c..f75698f58b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -18,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.text-edition-outline :refer [text-edition-outline]] [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] @@ -159,14 +160,14 @@ (>= zoom 8)) show-presence? page-id show-prototypes? (= options-mode :prototype) - show-selection-handlers? (seq selected) + show-selection-handlers? (and (seq selected) (not edition)) show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (seq selected)) show-snap-points? (and (or (contains? layout :dynamic-alignment) (contains? layout :snap-grid)) (or drawing-obj transform)) - show-selrect? (and selrect (empty? drawing)) + show-selrect? (and selrect (empty? drawing) (not edition)) show-measures? (and (not transform) (not node-editing?) show-distances?) show-artboard-names? (contains? layout :display-artboard-names) show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) @@ -294,6 +295,10 @@ :on-move-selected on-move-selected :on-context-menu on-menu-selected}]) + (when show-text-editor? + [:& text-edition-outline + {:shape (get base-objects edition)}]) + (when show-measures? [:& msr/measurement {:bounds vbox diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 7fe09003ca..a8890c0b44 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.viewport.hooks (:require - [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] @@ -18,6 +17,7 @@ [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] + [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as sfd] [app.main.ui.workspace.viewport.actions :as actions] [app.main.ui.workspace.viewport.utils :as utils] [app.main.worker :as uw] @@ -199,62 +199,18 @@ (defn setup-viewport-modifiers [modifiers objects] - (let [root-frame-ids (mf/use-memo (mf/deps objects) - #(->> objects - (vals) - (filter (fn [{:keys [type frame-id]}] - (and - (not= :frame type) - (= uuid/zero frame-id)))) - (map :id))) - - objects (select-keys objects root-frame-ids) - modifiers (select-keys modifiers root-frame-ids) - - 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)] - - ;; Layout effect is important so the code is executed before the modifiers - ;; are applied to the shape - (mf/use-layout-effect - (mf/deps transforms) - (fn [] - (when (and (nil? @prev-transforms) - (some? transforms)) - (utils/start-transform! globals/document shapes)) - - (when (some? modifiers) - (utils/update-transform! globals/document shapes transforms modifiers)) - - - (when (and (some? @prev-modifiers) - (not (some? modifiers))) - (utils/remove-transform! globals/document @prev-shapes)) - - (reset! prev-modifiers modifiers) - (reset! prev-transforms transforms) - (reset! prev-shapes shapes))))) + (let [frame? (into #{} (cph/get-frames-ids objects)) + ;; Removes from zero/shapes attribute all the frames so we can ask only for + ;; the non-frame children + objects (-> objects + (update-in [uuid/zero :shapes] #(filterv (comp not frame?) %)))] + (cph/get-children-ids objects uuid/zero)))) + modifiers (select-keys modifiers root-frame-ids)] + (sfd/use-dynamic-modifiers objects globals/document modifiers))) (defn inside-vbox [vbox objects frame-id] (let [frame (get objects frame-id)] diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index c2d0e8a3cf..03ee9365d1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -108,10 +108,7 @@ text? [shape-node - (dom/query shape-node "foreignObject") - (dom/query shape-node ".text-shape") - (dom/query shape-node ".text-svg") - (dom/query shape-node ".text-clip")] + (dom/query shape-node ".text-shape")] :else [shape-node]))) @@ -174,31 +171,18 @@ (let [transform (get transforms id) modifiers (get-in modifiers [id :modifiers]) - [text-transform text-width text-height] + [text-transform _text-width _text-height] (when (= :text type) - (text-corrected-transform shape transform modifiers)) - - text-width (str text-width) - text-height (str text-height)] + (text-corrected-transform shape transform modifiers))] (doseq [node nodes] (cond ;; Text shapes need special treatment because their resize only change ;; the text area, not the change size/position - (or (dom/class? node "text-shape") - (dom/class? node "text-svg")) + (dom/class? node "text-shape") (when (some? text-transform) (set-transform-att! node "transform" text-transform)) - (or (= (dom/get-tag-name node) "foreignObject") - (dom/class? node "text-clip")) - (let [cur-width (dom/get-attribute node "width") - cur-height (dom/get-attribute node "height")] - (when (and (some? text-width) (not= cur-width text-width)) - (dom/set-attribute! node "width" text-width)) - (when (and (some? text-height) (not= cur-height text-height)) - (dom/set-attribute! node "height" text-height))) - (or (= (dom/get-tag-name node) "mask") (= (dom/get-tag-name node) "filter")) (transform-region! node modifiers) diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index 1cbd600bf3..3e32493596 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -68,7 +68,7 @@ #{:app.main.data.workspace.notifications/handle-pointer-update :app.main.data.workspace.selection/change-hover-state}) -(defonce ^:dynamic *debug* (atom #{#_:events})) +(defonce ^:dynamic *debug* (atom #{#_:events :thumbnails})) (defn debug-all! [] (reset! *debug* debug-options)) (defn debug-none! [] (reset! *debug* #{}))