diff --git a/common/app/common/attrs.cljc b/common/app/common/attrs.cljc index 31fab1a22..d32311eea 100644 --- a/common/app/common/attrs.cljc +++ b/common/app/common/attrs.cljc @@ -9,71 +9,68 @@ (ns app.common.attrs) +;; Extract some attributes of a list of shapes. +;; For each attribute, if the value is the same in all shapes, +;; wll take this value. If there is any shape that is different, +;; the value of the attribute will be the keyword :multiple. +;; +;; If some shape has the value nil in any attribute, it's +;; considered a different value. If the shape does not contain +;; the attribute, it's ignored in the final result. +;; +;; Example: +;; (def shapes [{:stroke-color "#ff0000" +;; :stroke-width 3 +;; :fill-color "#0000ff" +;; :x 1000 :y 2000 :rx nil} +;; {:stroke-width "#ff0000" +;; :stroke-width 5 +;; :x 1500 :y 2000}]) +;; +;; (get-attrs-multi shapes [:stroke-color +;; :stroke-width +;; :fill-color +;; :rx +;; :ry]) +;; >>> {:stroke-color "#ff0000" +;; :stroke-width :multiple +;; :fill-color "#0000ff" +;; :rx nil +;; :ry nil} +;; + (defn get-attrs-multi - ([shapes attrs] (get-attrs-multi shapes attrs = identity)) - ([shapes attrs eq-fn sel-fn] - ;; Extract some attributes of a list of shapes. - ;; For each attribute, if the value is the same in all shapes, - ;; wll take this value. If there is any shape that is different, - ;; the value of the attribute will be the keyword :multiple. - ;; - ;; If some shape has the value nil in any attribute, it's - ;; considered a different value. If the shape does not contain - ;; the attribute, it's ignored in the final result. - ;; - ;; Example: - ;; (def shapes [{:stroke-color "#ff0000" - ;; :stroke-width 3 - ;; :fill-color "#0000ff" - ;; :x 1000 :y 2000 :rx nil} - ;; {:stroke-width "#ff0000" - ;; :stroke-width 5 - ;; :x 1500 :y 2000}]) - ;; - ;; (get-attrs-multi shapes [:stroke-color - ;; :stroke-width - ;; :fill-color - ;; :rx - ;; :ry]) - ;; >>> {:stroke-color "#ff0000" - ;; :stroke-width :multiple - ;; :fill-color "#0000ff" - ;; :rx nil - ;; :ry nil} - ;; - (let [defined-shapes (filter some? shapes) + ([objs attrs] + (get-attrs-multi objs attrs = identity)) - combine-value (fn [v1 v2] - (cond - (and (= v1 :undefined) (= v2 :undefined)) :undefined - (= v1 :undefined) (if (= v2 :multiple) :multiple (sel-fn v2)) - (= v2 :undefined) (if (= v1 :multiple) :multiple (sel-fn v1)) - (or (= v1 :multiple) (= v2 :multiple)) :multiple - (eq-fn v1 v2) (sel-fn v1) - :else :multiple)) + ([objs attrs eqfn sel] - combine-values (fn [attrs shape values] - (map #(combine-value (get shape % :undefined) - (get values % :undefined)) attrs)) + (loop [attr (first attrs) + attrs (rest attrs) + result (transient {})] - select-attrs (fn [shape attrs] - (zipmap attrs (map #(get shape % :undefined) attrs))) + (if attr + (let [value + (loop [curr (first objs) + objs (rest objs) + value ::undefined] - reducer (fn [result shape] - (zipmap attrs (combine-values attrs shape result))) + (if (and curr (not= value :multiple)) + ;; + (let [new-val (get curr attr ::undefined) + value (cond + (= new-val ::undefined) value + (= value ::undefined) (sel new-val) + (eqfn new-val value) value + :else :multiple)] + (recur (first objs) (rest objs) value)) + ;; + value))] + (recur (first attrs) + (rest attrs) + (cond-> result + (not= value ::undefined) + (assoc! attr value)))) - combined (reduce reducer - (select-attrs (first defined-shapes) attrs) - (rest defined-shapes)) + (persistent! result))))) - cleanup-value (fn [value] - (if (= value :undefined) nil value)) - - cleanup (fn [result] - (->> attrs - (map #(get result %)) - (zipmap attrs) - (filter #(not= (second %) :undefined)) - (into {})))] - - (cleanup combined)))) diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index d40ab6114..b19a00bcb 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -124,9 +124,6 @@ [shape {:keys [x y]}] (move shape (gpt/point (- x) (- y)))) -(defn translate-from-frame - [shape {:keys [x y]}] - (move shape (gpt/point x y))) ;; --- Helpers diff --git a/common/app/common/pages/changes.cljc b/common/app/common/pages/changes.cljc index 37375b218..681c40a26 100644 --- a/common/app/common/pages/changes.cljc +++ b/common/app/common/pages/changes.cljc @@ -29,12 +29,15 @@ (defmulti process-operation (fn [_ op] (:type op))) (defn process-changes - [data items] - (->> (us/verify ::spec/changes items) - (reduce #(do - ;; (prn "process-change" (:type %2) (:id %2)) - (or (process-change %1 %2) %1)) - data))) + ([data items] (process-changes data items true)) + ([data items verify?] + ;; When verify? false we spec the schema validation. Currently used to make just + ;; 1 validation even if the changes are applied twice + (when verify? + (us/verify ::spec/changes items)) + + (->> items + (reduce #(or (process-change %1 %2) %1) data)))) (defmethod process-change :set-option [data {:keys [page-id option value]}] @@ -91,7 +94,7 @@ (let [update-fn (fn [objects] (if-let [obj (get objects id)] (let [result (reduce process-operation obj operations)] - (us/verify ::spec/shape result) + #?(:clj (us/verify ::spec/shape result)) (assoc objects id result)) objects))] (if page-id diff --git a/frontend/deps.edn b/frontend/deps.edn index c7d7ca5e2..961c7459b 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -16,7 +16,7 @@ funcool/okulary {:mvn/version "2020.04.14-0"} funcool/potok {:mvn/version "3.2.0"} funcool/promesa {:mvn/version "6.0.0"} - funcool/rumext {:mvn/version "2020.11.27-0"} + funcool/rumext {:mvn/version "2021.01.26-0"} lambdaisland/uri {:mvn/version "1.4.54" :exclusions [org.clojure/data.json]} diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 50b70d826..857d27957 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -13,6 +13,7 @@ [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] + [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.worker :as uw] @@ -93,9 +94,10 @@ [:workspace-data] [:workspace-libraries file-id :data])] (try - (let [state (update-in state path1 cp/process-changes changes)] + (us/verify ::spec/changes changes) + (let [state (update-in state path1 cp/process-changes changes false)] (cond-> state - commit-local? (update-in path2 cp/process-changes changes))) + commit-local? (update-in path2 cp/process-changes changes false))) (catch :default e (vreset! error e) state)))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index be14de47e..c3ce7b831 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -221,40 +221,93 @@ (defn not-changed? [old-dim new-dim] (> (mth/abs (- old-dim new-dim)) 0.1)) -(defn resize-text [id new-width new-height] - (ptk/reify ::resize-text +(defn resize-text-batch [changes] + (ptk/reify ::resize-text-batch ptk/WatchEvent (watch [_ state stream] - (let [page-id (:current-page-id state) - shape (get-in state [:workspace-data :pages-index page-id :objects id]) - {:keys [selrect grow-type overflow-text]} (gsh/transform-shape shape) - {shape-width :width shape-height :height} selrect + (let [page-id (:current-page-id state) + + objects0 (get-in state [:workspace-file :data :pages-index page-id :objects]) + objects1 (get-in state [:workspace-data :pages-index page-id :objects]) + + change-text-shape + (fn [objects [id [new-width new-height]]] + + (let [shape (get objects id) + {:keys [selrect grow-type overflow-text]} (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) + + shape (cond-> shape + (and overflow-text (not= :fixed grow-type)) + (assoc :overflow-text false) + + (and (= :fixed grow-type) (not overflow-text) (> new-height shape-height)) + (assoc :overflow-text true) + + (and (= :fixed grow-type) overflow-text (<= new-height shape-height)) + (assoc :overflow-text true) + + (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)))] + (assoc objects id shape))) + undo-transaction (get-in state [:workspace-undo :transaction]) + objects2 (->> changes (reduce change-text-shape objects1)) - events - (cond-> [] - (and overflow-text (not= :fixed grow-type)) - (conj (update-overflow-text id false)) + regchg {:type :reg-objects + :page-id page-id + :shapes (vec (keys changes))} - (and (= :fixed grow-type) (not overflow-text) (> new-height shape-height)) - (conj (update-overflow-text id true)) + rchanges (dwc/generate-changes page-id objects1 objects2) + uchanges (dwc/generate-changes page-id objects2 objects0)] - (and (= :fixed grow-type) overflow-text (<= new-height shape-height)) - (conj (update-overflow-text id false)) - - (and (or (not-changed? shape-width new-width) (not-changed? shape-height new-height)) - (= grow-type :auto-width)) - (conj (dwt/update-dimensions [id] :width new-width) - (dwt/update-dimensions [id] :height new-height)) - - (and (not-changed? shape-height new-height) - (= grow-type :auto-height)) - (conj (dwt/update-dimensions [id] :height new-height)))] - - (if (not (empty? events)) + (if (seq rchanges) (rx/concat - (when (not undo-transaction) + (when-not undo-transaction (rx/of (dwc/start-undo-transaction))) - (rx/from events) - (when (not undo-transaction) + (rx/of (dwc/commit-changes (conj rchanges regchg) (conj uchanges regchg) {:commit-local? true})) + (when-not undo-transaction (rx/of (dwc/discard-undo-transaction))))))))) + +;; 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)))) + + ;; Stop buffering after time without resizes + stop-buffer (->> resize-events (rx/debounce 100))] + + (if-not (::handling-texts state) + (->> (rx/concat + (rx/of #(assoc % ::handling-texts true)) + (->> 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 %))) + + (rx/of #(dissoc % ::handling-texts)))) + (rx/empty)))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 635eb826f..dc04be45e 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -159,51 +159,45 @@ ids)) workspace-page-objects =)) +(def selected-data + (l/derived #(let [selected (get-in % [:workspace-local :selected]) + page-id (:current-page-id %) + objects (get-in % [:workspace-data :pages-index page-id :objects])] + (hash-map :selected selected + :page-id page-id + :objects objects)) + st/state =)) + (defn is-child-selected? [id] - (letfn [(selector [state] - (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - selected (get-in state [:workspace-local :selected]) - children (cp/get-children id objects)] + (letfn [(selector [{:keys [selected page-id objects]}] + (let [children (cp/get-children id objects)] (some #(contains? selected %) children)))] - (l/derived selector st/state))) + (l/derived selector selected-data =))) - -;; TODO: can be replaced by objects-by-id (def selected-objects - (letfn [(selector [state] - (let [selected (get-in state [:workspace-local :selected]) - page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects])] - (->> selected - (map #(get objects %)) - (filterv (comp not nil?)))))] - (l/derived selector st/state =))) + (letfn [(selector [{:keys [selected page-id objects]}] + (->> selected + (map #(get objects %)) + (filterv (comp not nil?))))] + (l/derived selector selected-data =))) (def selected-shapes-with-children - (letfn [(selector [state] - (let [selected (get-in state [:workspace-local :selected]) - page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - children (->> selected + (letfn [(selector [{:keys [selected page-id objects]}] + (let [children (->> selected (mapcat #(cp/get-children % objects)) (filterv (comp not nil?)))] (into selected children)))] - (l/derived selector st/state =))) - + (l/derived selector selected-data =))) (def selected-objects-with-children - (letfn [(selector [state] - (let [selected (get-in state [:workspace-local :selected]) - page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects]) - children (->> selected + (letfn [(selector [{:keys [selected page-id objects]}] + (let [children (->> selected (mapcat #(cp/get-children % objects)) (filterv (comp not nil?))) shapes (into selected children)] (mapv #(get objects %) shapes)))] - (l/derived selector st/state =))) + (l/derived selector selected-data =))) ;; ---- Viewer refs diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index da40a1ef7..295cceb7f 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -24,7 +24,8 @@ (let [shape (unchecked-get props "shape") background? (unchecked-get props "background?") {:keys [id x y width height]} (:selrect shape) - pdata (ugp/content->path (:content shape)) + content (:content shape) + pdata (mf/use-memo (mf/deps content) #(ugp/content->path content)) props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:d pdata}))] diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index c0198ca61..d46adeb29 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -23,33 +23,6 @@ ;; Context to store a re-mapping of the ids (def svg-ids-ctx (mf/create-context nil)) -(defn vbox->rect - "Converts the viewBox into a rectangle" - [vbox] - (when vbox - (let [[x y width height] (map ud/parse-float (str/split vbox " "))] - {:x x :y y :width width :height height}))) - -(defn vbox-center [shape] - (let [vbox-rect (-> (get-in shape [:content :attrs :viewBox] "0 0 100 100") - (vbox->rect))] - (gsh/center-rect vbox-rect))) - -(defn vbox-bounds [shape] - (let [vbox-rect (-> (get-in shape [:content :attrs :viewBox] "0 0 100 100") - (vbox->rect)) - vbox-center (gsh/center-rect vbox-rect) - transform (gsh/transform-matrix shape nil vbox-center)] - (-> (gsh/rect->points vbox-rect) - (gsh/transform-points vbox-center transform) - (gsh/points->rect))) ) - -(defn transform-viewbox [shape] - (let [center (vbox-center shape) - bounds (vbox-bounds shape) - {:keys [x y width height]} (gsh/center->rect center (:width bounds) (:height bounds))] - (str x " " y " " width " " height))) - (defn generate-id-mapping [content] (letfn [(visit-node [result node] (let [element-id (get-in node [:attrs :id]) @@ -58,79 +31,102 @@ (reduce visit-node result (:content node))))] (visit-node {} content))) + +(defonce replace-regex #"[^#]*#([^)\s]+).*") + +(defn replace-attrs-ids + "Replaces the ids inside a property" + [ids-mapping attrs] + + (letfn [(replace-ids [key val] + (if (map? val) + (cd/mapm replace-ids val) + (let [[_ from-id] (re-matches replace-regex val)] + (if (and from-id (contains? ids-mapping from-id)) + (str/replace val from-id (get ids-mapping from-id)) + val))))] + (cd/mapm replace-ids attrs))) + +(mf/defc svg-root + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") + + {:keys [x y width height]} shape + {:keys [tag attrs] :as content} (:content shape) + + ids-mapping (mf/use-memo #(generate-id-mapping content)) + + attrs (-> (clj->js attrs) + (obj/set! "x" x) + (obj/set! "y" y) + (obj/set! "width" width) + (obj/set! "height" height) + (obj/set! "preserveAspectRatio" "none"))] + + [:& (mf/provider svg-ids-ctx) {:value ids-mapping} + [:g.svg-raw {:transform (gsh/transform-matrix shape)} + [:> "svg" attrs children]]])) + +(mf/defc svg-element + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") + + {:keys [content]} shape + {:keys [attrs tag]} content + + ids-mapping (mf/use-ctx svg-ids-ctx) + attrs (mf/use-memo #(replace-attrs-ids ids-mapping attrs)) + custom-attrs (usa/extract-style-attrs shape) + + element-id (get-in content [:attrs :id]) + + style (obj/merge! (clj->js (:style attrs {})) + (obj/get custom-attrs "style")) + + attrs (-> (clj->js attrs) + (obj/merge! custom-attrs) + (obj/set! "style" style)) + + attrs (cond-> attrs + element-id (obj/set! "id" (get ids-mapping element-id))) + ] + [:> (name tag) attrs children])) + (defn svg-raw-shape [shape-wrapper] (mf/fnc svg-raw-shape - {::mf/wrap-props false} - [props] - (let [frame (unchecked-get props "frame") - shape (unchecked-get props "shape") - childs (unchecked-get props "childs") + {::mf/wrap-props false} + [props] - {:keys [tag attrs] :as content} (:content shape) + (let [frame (unchecked-get props "frame") + shape (unchecked-get props "shape") + childs (unchecked-get props "childs") - new-mapping (mf/use-memo #(when (= tag :svg) (generate-id-mapping content))) - ids-mapping (if (= tag :svg) - new-mapping - (mf/use-ctx svg-ids-ctx)) + {:keys [content]} shape + {:keys [tag]} content - rex #"[^#]*#([^)\s]+).*" + svg-root? (and (map? content) (= tag :svg)) + svg-tag? (map? content) + svg-leaf? (string? content)] - ;; Replaces the attributes ID's so there are no collisions between shapes - replace-ids - (fn replace-ids [key val] - (if (map? val) - (cd/mapm replace-ids val) - (let [[_ from-id] (re-matches rex val)] - (if (and from-id (contains? ids-mapping from-id)) - (str/replace val from-id (get ids-mapping from-id)) - val)))) + (cond + svg-root? + [:& svg-root {:shape shape} + (for [item childs] + [:& shape-wrapper {:frame frame :shape item :key (:id item)}])] - attrs (cd/mapm replace-ids attrs) + svg-tag? + [:& svg-element {:shape shape} + (for [item childs] + [:& shape-wrapper {:frame frame :shape item :key (:id item)}])] - custom-attrs (usa/extract-style-attrs shape) + svg-leaf? + content - style (obj/merge! (clj->js (:style attrs {})) - (obj/get custom-attrs "style")) - - attrs (-> (clj->js attrs) - (obj/merge! custom-attrs) - (obj/set! "style" style)) - - element-id (get-in content [:attrs :id])] - - (cond - ;; Root SVG TAG - (and (map? content) (= tag :svg)) - (let [{:keys [x y width height]} shape - attrs (-> attrs - (obj/set! "x" x) - (obj/set! "y" y) - (obj/set! "width" width) - (obj/set! "height" height) - (obj/set! "preserveAspectRatio" "none") - #_(obj/set! "viewBox" (transform-viewbox shape)))] - - [:& (mf/provider svg-ids-ctx) {:value ids-mapping} - [:g.svg-raw {:transform (gsh/transform-matrix shape)} - [:> "svg" attrs - (for [item childs] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])]]]) - - ;; Other tags different than root - (map? content) - (let [attrs (cond-> attrs - element-id (obj/set! "id" (get ids-mapping element-id)))] - [:> (name tag) attrs - (for [item childs] - [:& shape-wrapper {:frame frame - :shape item - :key (:id item)}])]) - - ;; String content - (string? content) content - - :else nil)))) + :else nil)))) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 6124f4575..6875ee50d 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -18,64 +18,90 @@ [app.util.object :as obj] [app.util.color :as uc] [app.main.ui.shapes.text.styles :as sts] - [app.main.ui.shapes.text.embed :as ste])) + [app.main.ui.shapes.text.embed :as ste] + [app.util.perf :as perf])) + +(mf/defc render-text + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + text (:text node) + style (sts/generate-text-styles props)] + [:span {:style style + :className (when (:fill-color-gradient node) "gradient")} + (if (= text "") "\u00A0" text)])) + +(mf/defc render-root + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + embed-fonts? (obj/get props "embed-fonts?") + children (obj/get props "children") + style (sts/generate-root-styles props)] + [:div.root.rich-text + {:style style + :xmlns "http://www.w3.org/1999/xhtml"} + [:* + [:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] + (when embed-fonts? + [ste/embed-fontfaces-style {:node node}])] + children])) + +(mf/defc render-paragraph-set + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + children (obj/get props "children") + style (sts/generate-paragraph-set-styles props)] + [:div.paragraph-set {:style style} children])) + +(mf/defc render-paragraph + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + children (obj/get props "children") + style (sts/generate-paragraph-styles props)] + [:p.paragraph {:style style} children])) ;; -- Text nodes -(mf/defc text-node - [{:keys [node index shape] :as props}] - (let [embed-resources? (mf/use-ctx muc/embed-ctx) - {:keys [type text children]} node - props #js {:shape shape} - render-node - (fn [index node] - (mf/element text-node {:index index - :node node - :key index - :shape shape}))] - +(mf/defc render-node + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + index (obj/get props "index") + {:keys [type text children]} node] (if (string? text) - (let [style (sts/generate-text-styles (clj->js node) props)] - [:span {:style style - :className (when (:fill-color-gradient node) "gradient")} - (if (= text "") "\u00A0" text)]) + [:> render-text props] - (let [children (map-indexed render-node children)] - (case type - "root" - (let [style (sts/generate-root-styles (clj->js node) props)] - [:div.root.rich-text - {:key index - :style style - :xmlns "http://www.w3.org/1999/xhtml"} - [:* - [:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"] - (when embed-resources? - [ste/embed-fontfaces-style {:node node}])] - children]) - - "paragraph-set" - (let [style (sts/generate-paragraph-set-styles (clj->js node) props)] - [:div.paragraph-set {:key index :style style} children]) - - "paragraph" - (let [style (sts/generate-paragraph-styles (clj->js node) props)] - [:p.paragraph {:key index :style style} children]) - - nil))))) + (let [component (case type + "root" render-root + "paragraph-set" render-paragraph-set + "paragraph" render-paragraph + nil)] + (when component + [:> component (obj/set! props "key" index) + (for [[index child] (d/enumerate children)] + (let [props (-> (obj/clone props) + (obj/set! "node" child) + (obj/set! "index" index) + (obj/set! "key" index))] + [:> render-node props]))]))))) (mf/defc text-content - {::mf/wrap-props false - ::mf/wrap [mf/memo]} + {::mf/wrap-props false} [props] (let [root (obj/get props "content") - shape (obj/get props "shape")] - [:& text-node {:index 0 - :node root - :shape shape}])) + shape (obj/get props "shape") + embed-fonts? (obj/get props "embed-fonts?")] + [:& render-node {:index 0 + :node root + :shape shape + :embed-fonts? embed-fonts?}])) (defn- retrieve-colors [shape] - (let [colors (->> shape :content + (let [colors (->> shape + :content (tree-seq map? :children) (into #{} (comp (map :fill-color) (filter string?))))] (if (empty? colors) @@ -87,8 +113,8 @@ ::mf/forward-ref true} [props ref] (let [shape (unchecked-get props "shape") - selected? (unchecked-get props "selected?") grow-type (unchecked-get props "grow-type") + embed-fonts? (mf/use-ctx muc/embed-ctx) {:keys [id x y width height content]} shape] [:foreignObject {:x x :y y @@ -99,4 +125,5 @@ :height (if (#{:auto-height :auto-width} grow-type) 10000 height) :ref ref} [:& text-content {:shape shape - :content (:content shape)}]])) + :content (:content shape) + :embed-fonts? embed-fonts?}]])) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 0ace63739..c859d2c0c 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -17,111 +17,115 @@ [app.util.text :as ut])) (defn generate-root-styles - [data props] - (let [valign (obj/get data "vertical-align" "top") - talign (obj/get data "text-align" "flex-start") - shape (obj/get props "shape") - base #js {:height (or (:height shape) "100%") - :width (or (:width shape) "100%") - :display "flex"}] - (cond-> base - (= valign "top") (obj/set! "alignItems" "flex-start") - (= valign "center") (obj/set! "alignItems" "center") - (= valign "bottom") (obj/set! "alignItems" "flex-end") + ([props] (generate-root-styles (clj->js (obj/get props "node")) props)) + ([data props] + (let [valign (obj/get data "vertical-align" "top") + talign (obj/get data "text-align" "flex-start") + shape (obj/get props "shape") + base #js {:height (or (:height shape) "100%") + :width (or (:width shape) "100%") + :display "flex"}] + (cond-> base + (= valign "top") (obj/set! "alignItems" "flex-start") + (= valign "center") (obj/set! "alignItems" "center") + (= valign "bottom") (obj/set! "alignItems" "flex-end") - (= talign "left") (obj/set! "justifyContent" "flex-start") - (= talign "center") (obj/set! "justifyContent" "center") - (= talign "right") (obj/set! "justifyContent" "flex-end") - (= talign "justify") (obj/set! "justifyContent" "stretch")))) + (= talign "left") (obj/set! "justifyContent" "flex-start") + (= talign "center") (obj/set! "justifyContent" "center") + (= talign "right") (obj/set! "justifyContent" "flex-end") + (= talign "justify") (obj/set! "justifyContent" "stretch"))))) (defn generate-paragraph-set-styles - [data props] - ;; The position absolute is used so the paragraph is "outside" - ;; the normal layout and can grow outside its parent - ;; We use this element to measure the size of the text - (let [base #js {:display "inline-block"}] - base)) + ([props] (generate-paragraph-set-styles nil props)) + ([data props] + ;; The position absolute is used so the paragraph is "outside" + ;; the normal layout and can grow outside its parent + ;; We use this element to measure the size of the text + (let [base #js {:display "inline-block"}] + base))) (defn generate-paragraph-styles - [data props] - (let [shape (obj/get props "shape") - grow-type (:grow-type shape) - base #js {:fontSize "14px" - :margin "inherit" - :lineHeight "1.2"} - lh (obj/get data "line-height") - ta (obj/get data "text-align")] - (cond-> base - ta (obj/set! "textAlign" ta) - lh (obj/set! "lineHeight" lh) - (= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))) + ([props] (generate-paragraph-styles (clj->js (obj/get props "node")) props)) + ([data props] + (let [shape (obj/get props "shape") + grow-type (:grow-type shape) + base #js {:fontSize "14px" + :margin "inherit" + :lineHeight "1.2"} + lh (obj/get data "line-height") + ta (obj/get data "text-align")] + (cond-> base + ta (obj/set! "textAlign" ta) + lh (obj/set! "lineHeight" lh) + (= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))) (defn generate-text-styles - [data props] - (let [letter-spacing (obj/get data "letter-spacing") - text-decoration (obj/get data "text-decoration") - text-transform (obj/get data "text-transform") - line-height (obj/get data "line-height") + ([props] (generate-text-styles (clj->js (obj/get props "node")) props)) + ([data props] + (let [letter-spacing (obj/get data "letter-spacing") + text-decoration (obj/get data "text-decoration") + text-transform (obj/get data "text-transform") + line-height (obj/get data "line-height") - font-id (obj/get data "font-id" (:font-id ut/default-text-attrs)) - font-variant-id (obj/get data "font-variant-id") + font-id (obj/get data "font-id" (:font-id ut/default-text-attrs)) + font-variant-id (obj/get data "font-variant-id") - font-family (obj/get data "font-family") - font-size (obj/get data "font-size") + font-family (obj/get data "font-family") + font-size (obj/get data "font-size") - ;; Old properties for backwards compatibility - fill (obj/get data "fill") - opacity (obj/get data "opacity" 1) + ;; Old properties for backwards compatibility + fill (obj/get data "fill") + opacity (obj/get data "opacity" 1) - fill-color (obj/get data "fill-color" fill) - fill-opacity (obj/get data "fill-opacity" opacity) - fill-color-gradient (obj/get data "fill-color-gradient" nil) - fill-color-gradient (when fill-color-gradient - (-> (js->clj fill-color-gradient :keywordize-keys true) - (update :type keyword))) + fill-color (obj/get data "fill-color" fill) + fill-opacity (obj/get data "fill-opacity" opacity) + fill-color-gradient (obj/get data "fill-color-gradient" nil) + fill-color-gradient (when fill-color-gradient + (-> (js->clj fill-color-gradient :keywordize-keys true) + (update :type keyword))) - ;; Uncomment this to allow to remove text colors. This could break the texts that already exist - ;;[r g b a] (if (nil? fill-color) - ;; [0 0 0 0] ;; Transparent color - ;; (uc/hex->rgba fill-color fill-opacity)) + ;; Uncomment this to allow to remove text colors. This could break the texts that already exist + ;;[r g b a] (if (nil? fill-color) + ;; [0 0 0 0] ;; Transparent color + ;; (uc/hex->rgba fill-color fill-opacity)) - [r g b a] (uc/hex->rgba fill-color fill-opacity) + [r g b a] (uc/hex->rgba fill-color fill-opacity) - text-color (if fill-color-gradient - (uc/gradient->css (js->clj fill-color-gradient)) - (str/format "rgba(%s, %s, %s, %s)" r g b a)) + text-color (if fill-color-gradient + (uc/gradient->css (js->clj fill-color-gradient)) + (str/format "rgba(%s, %s, %s, %s)" r g b a)) - fontsdb (deref fonts/fontsdb) + fontsdb (deref fonts/fontsdb) - base #js {:textDecoration text-decoration - :textTransform text-transform - :lineHeight (or line-height "inherit") - :color text-color - "--text-color" text-color}] + base #js {:textDecoration text-decoration + :textTransform text-transform + :lineHeight (or line-height "inherit") + :color text-color + "--text-color" text-color}] - (when (and (string? letter-spacing) - (pos? (alength letter-spacing))) - (obj/set! base "letterSpacing" (str letter-spacing "px"))) + (when (and (string? letter-spacing) + (pos? (alength letter-spacing))) + (obj/set! base "letterSpacing" (str letter-spacing "px"))) - (when (and (string? font-size) - (pos? (alength font-size))) - (obj/set! base "fontSize" (str font-size "px"))) + (when (and (string? font-size) + (pos? (alength font-size))) + (obj/set! base "fontSize" (str font-size "px"))) - (when (and (string? font-id) - (pos? (alength font-id))) - (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id)] - (let [font-family (or (:family font) - (obj/get data "fontFamily")) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (obj/get data "fontStyle")) - font-weight (or (:weight font-variant) - (obj/get data "fontWeight"))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight)))) + (when (and (string? font-id) + (pos? (alength font-id))) + (fonts/ensure-loaded! font-id) + (let [font (get fontsdb font-id)] + (let [font-family (or (:family font) + (obj/get data "fontFamily")) + font-variant (d/seek #(= font-variant-id (:id %)) + (:variants font)) + font-style (or (:style font-variant) + (obj/get data "fontStyle")) + font-weight (or (:weight font-variant) + (obj/get data "fontWeight"))] + (obj/set! base "fontFamily" font-family) + (obj/set! base "fontStyle" font-style) + (obj/set! base "fontWeight" font-weight)))) - base)) + base))) diff --git a/frontend/src/app/main/ui/workspace/frame_grid.cljs b/frontend/src/app/main/ui/workspace/frame_grid.cljs index eb4e3a4d3..bcec357b8 100644 --- a/frontend/src/app/main/ui/workspace/frame_grid.cljs +++ b/frontend/src/app/main/ui/workspace/frame_grid.cljs @@ -15,35 +15,36 @@ [app.common.math :as mth] [app.common.pages :as cp] [app.common.geom.shapes :as gsh] - [app.util.geom.grid :as gg])) + [app.util.geom.grid :as gg] + [app.common.uuid :as uuid])) (mf/defc square-grid [{:keys [frame zoom grid] :as props}] - (let [{:keys [color size] :as params} (-> grid :params) + (let [grid-id (mf/use-memo #(uuid/next)) + {:keys [color size] :as params} (-> grid :params) {color-value :color color-opacity :opacity} (-> grid :params :color) ;; Support for old color format - color-value (or color-value (:value (get-in grid [:params :color :value]))) - {frame-width :width frame-height :height :keys [x y]} frame] - (when (> size 0) - [:g.grid - [:* - (for [xs (range size frame-width size)] - [:line {:key (str (:id frame) "-y-" xs) - :x1 (mth/round (+ x xs)) - :y1 (mth/round y) - :x2 (mth/round (+ x xs)) - :y2 (mth/round (+ y frame-height)) - :style {:stroke color-value - :stroke-opacity color-opacity - :stroke-width (str (/ 1 zoom))}}]) - (for [ys (range size frame-height size)] - [:line {:key (str (:id frame) "-x-" ys) - :x1 (mth/round x) - :y1 (mth/round (+ y ys)) - :x2 (mth/round (+ x frame-width)) - :y2 (mth/round (+ y ys)) - :style {:stroke color-value - :stroke-opacity color-opacity - :stroke-width (str (/ 1 zoom))}}])]]))) + color-value (or color-value (:value (get-in grid [:params :color :value])))] + + [:g.grid + [:defs + [:pattern {:id grid-id + :x (:x frame) + :y (:y frame) + :width size + :height size + :pattern-units "userSpaceOnUse"} + [:path {:d (str "M " size " " 0 " " + "L " 0 " " 0 " " 0 " " size " ") + :style {:fill "none" + :stroke color-value + :stroke-opacity color-opacity + :stroke-width (str (/ 1 zoom))}}]]] + + [:rect {:x (:x frame) + :y (:y frame) + :width (:width frame) + :height (:height frame) + :fill (str "url(#" grid-id ")")}]])) (mf/defc layout-grid [{:keys [key frame zoom grid]}] (let [{color-value :color color-opacity :opacity} (-> grid :params :color) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index 76f57dbdc..f2b916974 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -359,6 +359,7 @@ :zoom zoom}])])) (mf/defc selection-handlers + {::mf/wrap [mf/memo]} [{:keys [selected edition zoom show-distances] :as props}] (let [;; We need remove posible nil values because on shape ;; deletion many shape will reamin selected and deleted diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index ef7094bf2..51f9e51e6 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -46,17 +46,6 @@ (def image-wrapper (common/generic-wrapper-factory image/image-shape)) (def rect-wrapper (common/generic-wrapper-factory rect/rect-shape)) -(defn- shape-wrapper-memo-equals? - [np op] - (let [n-shape (obj/get np "shape")] - (if (= (:type n-shape) :group) - false - (let [o-shape (obj/get op "shape") - n-frame (obj/get np "frame") - o-frame (obj/get op "frame")] - (and (identical? n-shape o-shape) - (identical? n-frame o-frame)))))) - (defn make-is-moving-ref [id] (fn [] @@ -66,7 +55,7 @@ (l/derived check-moving refs/workspace-local)))) (mf/defc shape-wrapper - {::mf/wrap [#(mf/memo' % shape-wrapper-memo-equals?)] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] ::mf/wrap-props false} [props] (let [shape (obj/get props "shape") @@ -77,8 +66,6 @@ opts #js {:shape shape :frame frame} - alt? (hooks/use-rxsub ms/keyboard-alt) - moving-iref (mf/use-memo (mf/deps (:id shape)) (make-is-moving-ref (:id shape))) moving? (mf/deref moving-iref) svg-element? (and (= (:type shape) :svg-raw) @@ -88,8 +75,7 @@ (when (and shape (not (:hidden shape))) [:* (if-not svg-element? - [:g.shape-wrapper {:style {:display (when hide-moving? "none") - :cursor (if alt? cur/duplicate nil)}} + [:g.shape-wrapper {:style {:display (when hide-moving? "none")}} (case (:type shape) :path [:> path/path-wrapper opts] :text [:> text/text-wrapper opts] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 0afcd9c19..bec720cb5 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -25,24 +25,6 @@ [rumext.alpha :as mf] [app.main.ui.context :as muc])) -(defn- frame-wrapper-factory-equals? - [np op] - (let [n-shape (aget np "shape") - o-shape (aget op "shape") - n-objs (aget np "objects") - o-objs (aget op "objects") - - ids (:shapes n-shape)] - (and (identical? n-shape o-shape) - (loop [id (first ids) - ids (rest ids)] - (if (nil? id) - true - (if (identical? (get n-objs id) - (get o-objs id)) - (recur (first ids) (rest ids)) - false)))))) - (defn use-select-shape [{:keys [id]} edition] (mf/use-callback (mf/deps id edition) @@ -121,7 +103,7 @@ [shape-wrapper] (let [frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % frame-wrapper-factory-equals?) custom-deferred] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "objects"])) custom-deferred] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index 7c6f464cd..f3f4cc3c5 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -21,15 +21,6 @@ [app.common.geom.shapes :as gsh] [app.util.debug :refer [debug?]])) -(defn- group-wrapper-factory-equals? - [np op] - (let [n-shape (unchecked-get np "shape") - o-shape (unchecked-get op "shape") - n-frame (unchecked-get np "frame") - o-frame (unchecked-get op "frame")] - (and (= n-frame o-frame) - (= n-shape o-shape)))) - (defn use-double-click [{:keys [id]}] (mf/use-callback (mf/deps id) @@ -42,7 +33,7 @@ [shape-wrapper] (let [group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % group-wrapper-factory-equals?)] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 4a92c4b0c..3e26cca55 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -21,20 +21,11 @@ ;; this allows them to have gradients, shadows and masks (def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath}) -(defn- svg-raw-wrapper-factory-equals? - [np op] - (let [n-shape (unchecked-get np "shape") - o-shape (unchecked-get op "shape") - n-frame (unchecked-get np "frame") - o-frame (unchecked-get op "frame")] - (and (= n-frame o-frame) - (= n-shape o-shape)))) - (defn svg-raw-wrapper-factory [shape-wrapper] (let [svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)] (mf/fnc svg-raw-wrapper - {::mf/wrap [#(mf/memo' % svg-raw-wrapper-factory-equals?)] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 40f1ba745..212979b81 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -34,46 +34,33 @@ ;; --- Events -(defn use-double-click [{:keys [id]} selected?] +(defn use-double-click [{:keys [id]}] (mf/use-callback - (mf/deps id selected?) + (mf/deps id) (fn [event] (dom/stop-propagation event) (dom/prevent-default event) - (when selected? - (st/emit! (dw/start-edition-mode id)))))) + (st/emit! (dw/start-edition-mode id))))) ;; --- Text Wrapper for workspace -(mf/defc text-wrapper +(mf/defc text-static-content + [{:keys [shape]}] + [:& text/text-shape {:shape shape + :grow-type (:grow-type shape)}]) + +(mf/defc text-resize-content {::mf/wrap-props false} [props] - (let [{:keys [id name x y width height grow-type] :as shape} (unchecked-get props "shape") - ghost? (mf/use-ctx muc/ghost-ctx) - selected-iref (mf/use-memo (mf/deps (:id shape)) - #(refs/make-selected-ref (:id shape))) - selected? (mf/deref selected-iref) - edition (mf/deref refs/selected-edition) - current-transform (mf/deref refs/current-transform) - - render-editor (mf/use-state false) - - edition? (= edition id) - embed-resources? (mf/use-ctx muc/embed-ctx) - - handle-mouse-down (we/use-mouse-down shape) - handle-context-menu (we/use-context-menu shape) - handle-pointer-enter (we/use-pointer-enter shape) - handle-pointer-leave (we/use-pointer-leave shape) - handle-double-click (use-double-click shape selected?) - + (let [shape (obj/get props "shape") + {:keys [id name x y grow-type]} shape paragraph-ref (mf/use-state nil) handle-resize-text (mf/use-callback (mf/deps id) (fn [entries] - (when (and (not ghost?) (seq 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 @@ -97,24 +84,41 @@ (mf/use-effect (mf/deps @paragraph-ref handle-resize-text grow-type) (fn [] - (when (not ghost?) - (when-let [paragraph-node @paragraph-ref] - (let [observer (js/ResizeObserver. handle-resize-text)] - (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) - (.observe observer paragraph-node) - #(.disconnect observer)))))) + (when-let [paragraph-node @paragraph-ref] + (let [observer (js/ResizeObserver. handle-resize-text)] + (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) + (.observe observer paragraph-node) + #(.disconnect observer))))) + [:& text/text-shape {:ref text-ref-cb + :shape shape + :grow-type (:grow-type shape)}])) + +(mf/defc text-wrapper + {::mf/wrap-props false} + [props] + (let [{:keys [id x y width height] :as shape} (unchecked-get props "shape") + ghost? (mf/use-ctx muc/ghost-ctx) + edition (mf/deref refs/selected-edition) + edition? (= edition id) + + handle-mouse-down (we/use-mouse-down shape) + handle-context-menu (we/use-context-menu shape) + handle-pointer-enter (we/use-pointer-enter shape) + handle-pointer-leave (we/use-pointer-leave shape) + handle-double-click (use-double-click shape)] [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect acordingly [:g.text-shape {:opacity (when edition? 0) :pointer-events "none"} - [:& text/text-shape {:key (str "text-shape" (:id shape)) - :ref text-ref-cb - :shape shape - :selected? selected? - :grow-type (:grow-type shape)}]] + + (if ghost? + [:& text-static-content {:shape shape}] + [:& text-resize-content {:shape shape}])] + + (when (and (not ghost?) edition?) [:& editor/text-shape-edit {:key (str "editor" (:id shape)) :shape shape}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 82c8fdb3d..b152fb9ae 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -237,40 +237,13 @@ :objects objects :key (:id item)}]))])])) -(defn frame-wrapper-memo-equals? - [oprops nprops] - (let [new-sel (unchecked-get nprops "selected") - old-sel (unchecked-get oprops "selected") - new-itm (unchecked-get nprops "item") - old-itm (unchecked-get oprops "item") - new-idx (unchecked-get nprops "index") - old-idx (unchecked-get oprops "index") - new-obs (unchecked-get nprops "objects") - old-obs (unchecked-get oprops "objects")] - (and (= new-itm old-itm) - (identical? new-idx old-idx) - (let [childs (cp/get-children (:id new-itm) new-obs) - childs' (conj childs (:id new-itm))] - (and (or (= new-sel old-sel) - (not (or (boolean (some new-sel childs')) - (boolean (some old-sel childs'))))) - (loop [ids (rest childs) - id (first childs)] - (if (nil? id) - true - (if (= (get new-obs id) - (get old-obs id)) - (recur (rest ids) - (first ids)) - false)))))))) - ;; This components is a piece for sharding equality check between top ;; level frames and try to avoid rerender frames that are does not ;; affected by the selected set. (mf/defc frame-wrapper {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % frame-wrapper-memo-equals?) + ::mf/wrap [#(mf/memo' % (mf/check-props ["selected" "item" "index" "objects"])) #(mf/deferred % ts/idle-then-raf)]} [props] [:> layer-item props]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs index 7ced7afb9..4c07e1cc2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/fill.cljs @@ -28,20 +28,8 @@ :fill-color-ref-file :fill-color-gradient]) -(defn- fill-menu-props-equals? - [np op] - (let [new-ids (obj/get np "ids") - old-ids (obj/get op "ids") - new-editor (obj/get np "editor") - old-editor (obj/get op "editor") - new-values (obj/get np "values") - old-values (obj/get op "values")] - (and (= new-ids old-ids) - (= new-editor old-editor) - (every? #(identical? (% new-values) (% old-values)) fill-attrs)))) - (mf/defc fill-menu - {::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]} + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "editor" "values"]))]} [{:keys [ids type values editor] :as props}] (let [locale (mf/deref i18n/locale) show? (or (not (nil? (:fill-color values))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs index 9ac5e9693..586ad2906 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs @@ -154,7 +154,7 @@ (reduce extract-attrs [] shapes))) (mf/defc options - {::mf/wrap [mf/memo] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "shapes-with-children"]))] ::mf/wrap-props false} [props] (let [shapes (unchecked-get props "shapes") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs index 740e491da..eb5441a8e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/stroke.cljs @@ -33,15 +33,6 @@ :stroke-opacity :stroke-color-gradient]) -(defn- stroke-menu-props-equals? - [np op] - (let [new-ids (obj/get np "ids") - old-ids (obj/get op "ids") - new-values (obj/get np "values") - old-values (obj/get op "values")] - (and (= new-ids old-ids) - (every? #(identical? (% new-values) (% old-values)) stroke-attrs)))) - (defn- width->string [width] (if (= width :multiple) "" @@ -55,7 +46,7 @@ (pr-str value))) (mf/defc stroke-menu - {::mf/wrap [#(mf/memo' % stroke-menu-props-equals?)]} + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]} [{:keys [ids type values] :as props}] (let [locale (i18n/use-locale) label (case type diff --git a/frontend/src/app/main/ui/workspace/snap_distances.cljs b/frontend/src/app/main/ui/workspace/snap_distances.cljs index 41d36fba0..966c5f388 100644 --- a/frontend/src/app/main/ui/workspace/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/snap_distances.cljs @@ -267,11 +267,9 @@ (mf/defc snap-distances {::mf/wrap-props false} [props] - (let [layout (unchecked-get props "layout") - page-id (unchecked-get props "page-id") + (let [page-id (unchecked-get props "page-id") zoom (unchecked-get props "zoom") selected (unchecked-get props "selected") - transform (unchecked-get props "transform") selected-shapes (mf/deref (refs/objects-by-id selected)) frame-id (-> selected-shapes first :frame-id) frame (mf/deref (refs/object-by-id frame-id)) @@ -280,23 +278,20 @@ update-shape (fn [shape] (-> shape (update :modifiers merge (:modifiers local)) gsh/transform-shape))] - (when (and (contains? layout :dynamic-alignment) - (= transform :move) - (not (empty? selected))) - (let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect) - key (->> selected (map str) (str/join "-"))] - [:g.distance - [:& shape-distance - {:selrect selrect - :page-id page-id - :frame frame - :zoom zoom - :coord :x - :selected selected}] - [:& shape-distance - {:selrect selrect - :page-id page-id - :frame frame - :zoom zoom - :coord :y - :selected selected}]])))) + (let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect) + key (->> selected (map str) (str/join "-"))] + [:g.distance + [:& shape-distance + {:selrect selrect + :page-id page-id + :frame frame + :zoom zoom + :coord :x + :selected selected}] + [:& shape-distance + {:selrect selrect + :page-id page-id + :frame frame + :zoom zoom + :coord :y + :selected selected}]]))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 32d630f6d..04c2b48cf 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -188,7 +188,8 @@ :edition edition}]])) (mf/defc ghost-frames - {::mf/wrap-props false} + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [props] (let [modifiers (obj/get props "modifiers") selected (obj/get props "selected") @@ -249,6 +250,7 @@ (gsh/selection-rect)) alt? (mf/use-state false) + cursor (mf/use-state cur/pointer-inner) viewport-ref (mf/use-ref nil) zoom-view-ref (mf/use-ref nil) last-position (mf/use-var nil) @@ -260,6 +262,13 @@ drawing-path? (and edition (= :draw (get-in edit-path [edition :edit-mode]))) zoom (or zoom 1) + show-grids? (contains? layout :display-grid) + show-snap-points? (and (contains? layout :dynamic-alignment) + (or drawing-obj (:transform local))) + show-snap-distance? (and (contains? layout :dynamic-alignment) + (= (:transform local) :move) + (not (empty? selected))) + on-mouse-down (mf/use-callback (mf/deps drawing-tool edition) @@ -590,6 +599,28 @@ ;; We schedule the event so it fires after `initialize-page` event (timers/schedule #(st/emit! (dw/initialize-viewport size)))))) + ;; This change is in an effect to minimize the sideffects of the cursor chaning + ;; Changing a cursor will produce a "reflow" so we defer it until the component is rendered + (mf/use-layout-effect + (mf/deps @cursor @alt? panning drawing-tool drawing-path?) + (fn [] + (let [new-cursor + (cond + panning cur/hand + (= drawing-tool :comments) cur/comments + (= drawing-tool :frame) cur/create-artboard + (= drawing-tool :rect) cur/create-rectangle + (= drawing-tool :circle) cur/create-ellipse + (or (= drawing-tool :path) drawing-path?) cur/pen + (= drawing-tool :curve) cur/pencil + drawing-tool cur/create-shape + @alt? cur/duplicate + :else cur/pointer-inner)] + + (when (not= @cursor new-cursor) + (timers/raf + #(reset! cursor new-cursor)))))) + (mf/use-layout-effect (mf/deps layout) on-resize) (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) @@ -619,16 +650,7 @@ :view-box (format-viewbox vbox) :ref viewport-ref :class (when drawing-tool "drawing") - :style {:cursor (cond - panning cur/hand - (= drawing-tool :comments) cur/comments - (= drawing-tool :frame) cur/create-artboard - (= drawing-tool :rect) cur/create-rectangle - (= drawing-tool :circle) cur/create-ellipse - (or (= drawing-tool :path) drawing-path?) cur/pen - (= drawing-tool :curve) cur/pencil - drawing-tool cur/create-shape - :else cur/pointer-inner) + :style {:cursor @cursor :background-color (get options :background "#E8E9EA")} :on-context-menu on-context-menu :on-click on-click @@ -671,22 +693,24 @@ :tool drawing-tool :modifiers (:modifiers local)}]) - (when (contains? layout :display-grid) + (when show-grids? [:& frame-grid {:zoom zoom}]) - [:& snap-points {:layout layout - :transform (:transform local) - :drawing drawing-obj - :zoom zoom - :page-id page-id - :selected selected - :local local}] + (when show-snap-points? + [:& snap-points {:layout layout + :transform (:transform local) + :drawing drawing-obj + :zoom zoom + :page-id page-id + :selected selected + :local local}]) - [:& snap-distances {:layout layout - :zoom zoom - :transform (:transform local) - :selected selected - :page-id page-id}] + (when show-snap-distance? + [:& snap-distances {:layout layout + :zoom zoom + :transform (:transform local) + :selected selected + :page-id page-id}]) (when tooltip [:& cursor-tooltip {:zoom zoom :tooltip tooltip}])] @@ -697,7 +721,9 @@ [:& interactions {:selected selected}])]])) -(mf/defc viewport-actions [] +(mf/defc viewport-actions + {::mf/wrap [mf/memo]} + [] (let [edition (mf/deref refs/selected-edition) selected (mf/deref refs/selected-objects) shape (-> selected first)] diff --git a/frontend/src/app/util/text.cljs b/frontend/src/app/util/text.cljs index 607e9251c..ef75f6535 100644 --- a/frontend/src/app/util/text.cljs +++ b/frontend/src/app/util/text.cljs @@ -93,12 +93,31 @@ (rec-fn {} node))) +(defn content->nodes [node] + (loop [result (transient []) + curr node + pending (transient [])] + + (let [result (conj! result curr)] + ;; Adds children to the pending list + (let [children (:children curr) + pending (loop [child (first children) + children (rest children) + pending pending] + (if child + (recur (first children) + (rest children) + (conj! pending child)) + pending))] + + (if (= 0 (count pending)) + (persistent! result) + ;; Iterates with the next value in pending + (let [next (get pending (dec (count pending)))] + (recur result next (pop! pending)))))))) + (defn get-text-attrs-multi [node attrs] - (let [rec-fn - (fn rec-fn [current node] - (let [current (reduce rec-fn current (:children node []))] - (get-attrs-multi [current node] attrs)))] - (merge (select-keys default-text-attrs attrs) - (rec-fn {} node)))) + (let [nodes (content->nodes node)] + (get-attrs-multi nodes attrs)))