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/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 6124f4575..467903f3b 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -18,64 +18,89 @@ [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 (-> props + (obj/set! "node" child) + (obj/set! "index" 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 +112,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 +124,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/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/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)))