diff --git a/frontend/src/app/main/ui/hooks/mutable_observer.cljs b/frontend/src/app/main/ui/hooks/mutable_observer.cljs new file mode 100644 index 000000000..42f8b2c1b --- /dev/null +++ b/frontend/src/app/main/ui/hooks/mutable_observer.cljs @@ -0,0 +1,54 @@ +;; 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.hooks.mutable-observer + (:require + [app.common.logging :as log] + [rumext.alpha :as mf])) + +(log/set-level! :warn) + +(defn use-mutable-observer + [on-change] + + (let [prev-obs-ref (mf/use-ref nil) + node-ref (mf/use-ref nil) + + on-mutation + (mf/use-callback + (mf/deps on-change) + (fn [mutation] + (log/debug :action "mutation" :js/mutation mutation) + (on-change (mf/ref-val node-ref)))) + + set-node + (mf/use-callback + (mf/deps on-mutation) + (fn [^js node] + (when (and (some? node) (not= (mf/ref-val node-ref) node)) + (mf/set-ref-val! node-ref node) + + (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] + (.disconnect prev-obs) + (mf/set-ref-val! prev-obs-ref nil)) + + (when (some? node) + (let [options #js {:attributes true + :childList true + :subtree true + :characterData true} + mutation-obs (js/MutationObserver. on-mutation)] + (mf/set-ref-val! prev-obs-ref mutation-obs) + (.observe mutation-obs node options))))))] + + (mf/use-effect + (fn [] + (fn [] + (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] + (.disconnect prev-obs) + (mf/set-ref-val! prev-obs-ref nil))))) + + set-node)) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index 8396ae47b..1eb580653 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.shapes.gradients (:require + [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -106,6 +107,7 @@ :gradient gradient :shape shape}] (when gradient - (case (:type gradient) - :linear [:> linear-gradient gradient-props] - :radial [:> radial-gradient gradient-props])))) + (case (d/name (:type gradient)) + "linear" [:> linear-gradient gradient-props] + "radial" [:> radial-gradient gradient-props] + nil)))) 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 ae9929b7d..eb39b1edf 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -9,7 +9,6 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.geom.shapes :as geom] - [app.common.transit :as transit] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] @@ -24,12 +23,7 @@ (let [node (obj/get props "node") text (:text node) style (sts/generate-text-styles node)] - [:span.text-node {:style style - :data-fill-color (:fill-color node) - :data-fill-color-gradient (transit/encode-str (:fill-color-gradient node)) - :data-fill-color-ref-file (transit/encode-str (:fill-color-ref-file node)) - :data-fill-color-ref-id (transit/encode-str (:fill-color-ref-id node)) - :data-fill-opacity (:fill-opacity node)} + [:span.text-node {:style style} (if (= text "") "\u00A0" text)])) (mf/defc render-root @@ -193,7 +187,10 @@ {::mf/wrap-props false ::mf/forward-ref true} [props ref] - (let [{:keys [id x y width height content] :as shape} (obj/get props "shape") + (let [shape (obj/get props "shape") + transform (str (geom/transform-matrix shape)) + + {:keys [id x y width height content]} shape grow-type (obj/get props "grow-type") ;; This is only needed in workspace ;; We add 8px to add a padding for the exporter ;; width (+ width 8) @@ -205,16 +202,17 @@ plain-colors? (remap-colors color-mapping))] - [:foreignObject {:x x - :y y - :id id - :data-colors (->> colors (str/join ",")) - :data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify)) - :transform (geom/transform-matrix shape) - :width (if (#{:auto-width} grow-type) 100000 width) - :height (if (#{:auto-height :auto-width} grow-type) 100000 height) - :style (-> (obj/new) (attrs/add-layer-props shape)) - :ref ref} + [:foreignObject + {:x x + :y y + :id id + :data-colors (->> colors (str/join ",")) + :data-mapping (-> color-mapping-inverse (clj->js) (js/JSON.stringify)) + :transform transform + :width (if (#{:auto-width} grow-type) 100000 width) + :height (if (#{:auto-height :auto-width} grow-type) 100000 height) + :style (-> (obj/new) (attrs/add-layer-props shape)) + :ref ref} ;; We use a class here because react has a bug that won't use the appropriate selector for ;; `background-clip` [:style ".text-node { background-clip: text; diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 2af461501..0d5e98d8d 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.text :as txt] + [app.common.transit :as transit] [app.main.fonts :as fonts] [app.util.color :as uc] [app.util.object :as obj] @@ -71,31 +72,22 @@ fill-color (:fill-color data) fill-opacity (:fill-opacity data) - ;; 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) text-color (when (and (some? fill-color) (some? fill-opacity)) (str/format "rgba(%s, %s, %s, %s)" r g b a)) + fontsdb (deref fonts/fontsdb) base #js {:textDecoration text-decoration :textTransform text-transform :lineHeight (or line-height "inherit") - :color text-color - :caretColor "black"}] + :color "transparent" + :caretColor (or text-color "black")} - (when-let [gradient (:fill-color-gradient data)] - (let [text-color (-> (update gradient :type keyword) - (uc/gradient->css))] - (-> base - (obj/set! "color" text-color) - #_(obj/set! "--text-color" text-color) - #_(obj/set! "backgroundImage" "var(--text-color)") - #_(obj/set! "WebkitTextFillColor" "transparent") - #_(obj/set! "WebkitBackgroundClip" "text")))) + base (-> base + (obj/set! "--fill-color" fill-color) + (obj/set! "--fill-color-gradient" (transit/encode-str (:fill-color-gradient data))) + (obj/set! "--fill-opacity" fill-opacity))] (when (and (string? letter-spacing) (pos? (alength letter-spacing))) 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 607860b1a..145723ec2 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -45,10 +45,7 @@ [:> :g group-props [:defs [:clipPath {:id clip-id} - [:rect.text-clip - {:x x :y y - :width width :height height - :transform (gsh/transform-matrix shape)}]]] + [:rect.text-clip {:x x :y y :width width :height height}]]] (for [[index data] (d/enumerate position-data)] (let [props (-> #js {:x (:x data) :y (:y data) @@ -63,3 +60,5 @@ :whiteSpace "pre"} (attrs/add-fill data (get-gradient-id index)))})] [:> :text props (:text data)]))]]])) + + diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index cefb85a50..c31e71ad8 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -7,16 +7,15 @@ (ns app.main.ui.workspace.shapes.text (:require [app.common.attrs :as attrs] - [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.shapes :as gsh] [app.common.logging :as log] [app.common.math :as mth] - [app.common.transit :as transit] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text.fo-text :as fo] [app.main.ui.shapes.text.svg-text :as svg] @@ -39,7 +38,7 @@ (mf/defc text-static-content [{:keys [shape]}] [:& fo/text-shape {:shape shape - :grow-type (:grow-type shape)}]) + :grow-type (:grow-type shape)}]) (defn- update-with-current-editor-state [{:keys [id] :as shape}] @@ -114,36 +113,10 @@ (mf/use-effect (fn [] #(mf/set-ref-val! mnt false))) - [:& fo/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}])) - - -(defn calc-position-data - [base-node] - (let [viewport (dom/get-element "render") - zoom (get-in @st/state [:workspace-local :zoom]) - text-data (utp/calc-text-node-positions base-node viewport zoom)] - (->> text-data - (map (fn [{:keys [node position text]}] - (let [{:keys [x y width height]} position - rtl? (= "rtl" (.-dir (.-parentElement ^js node))) - styles (.computedStyleMap ^js node)] - (d/without-nils - {:rtl? rtl? - :x (if rtl? (+ x width) x) - :y (+ y height) - :width width - :height height - :font-family (str (.get styles "font-family")) - :font-size (str (.get styles "font-size")) - :font-weight (str (.get styles "font-weight")) - :text-transform (str (.get styles "text-transform")) - :text-decoration (str (.get styles "text-decoration")) - :font-style (str (.get styles "font-style")) - :fill-color (or (dom/get-attribute node "data-fill-color") "#000000") - :fill-color-gradient (transit/decode-str (dom/get-attribute node "data-fill-color-gradient")) - :fill-opacity (d/parse-double (or (:fill-opacity node) "1")) - :text text}))))))) - + [:& fo/text-shape {:ref text-ref-cb + :shape shape + :grow-type (:grow-type shape) + :key (str "shape-" (:id shape))}])) (mf/defc text-wrapper @@ -154,13 +127,13 @@ edition? (mf/deref edition-ref) shape-ref (mf/use-ref nil) - prev-obs-ref (mf/use-ref nil) local-position-data (mf/use-state nil) handle-change-foreign-object - (fn [] - (when-let [node (mf/ref-val shape-ref)] - (let [position-data (calc-position-data node) + (fn [node] + (when (some? node) + (mf/set-ref-val! shape-ref node) + (let [position-data (utp/calc-position-data node) parent (dom/get-parent node) parent-transform (dom/get-attribute parent "transform") node-transform (dom/get-attribute node "transform") @@ -173,40 +146,20 @@ mtx (-> (gmt/multiply parent-mtx node-mtx) (gmt/inverse)) - position-data' + position-data (->> position-data (mapv #(merge % (-> (select-keys % [:x :y :width :height]) (gsh/transform-rect mtx)))))] - (reset! local-position-data position-data')))) + (reset! local-position-data position-data)))) - on-change-node - (fn [^js node] - (mf/set-ref-val! shape-ref node) - - (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] - (.disconnect prev-obs) - (mf/set-ref-val! prev-obs-ref nil)) - - (when (some? node) - (let [fo-node (dom/query node "foreignObject") - options #js {:attributes true - :childList true - :subtree true} - mutation-obs (js/MutationObserver. handle-change-foreign-object)] - (mf/set-ref-val! prev-obs-ref mutation-obs) - (.observe mutation-obs fo-node options))))] - (mf/use-effect - (fn [] - (fn [] - (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] - (.disconnect prev-obs) - (mf/set-ref-val! prev-obs-ref nil))))) + on-change-node (use-mutable-observer handle-change-foreign-object)] + ;; When the text is "dirty?" we get recalculate the positions (mf/use-layout-effect (mf/deps id dirty?) (fn [] (let [node (mf/ref-val shape-ref) - position-data (calc-position-data node)] + position-data (utp/calc-position-data node)] (reset! local-position-data nil) (st/emit! (dch/update-shapes [id] @@ -233,9 +186,10 @@ :edition? edition? :key (str id edition?)}]] - [:g.text-svg {:opacity (when edition? 0) - :pointer-events "none"} - (when (some? (:position-data shape)) - [:& svg/text-shape {:shape (cond-> shape - (some? @local-position-data) - (assoc :position-data @local-position-data))}])]]])) + (when (and (not edition?) (or (some? (:position-data shape)) (some? local-position-data))) + (let [shape + (cond-> shape + (some? @local-position-data) + (assoc :position-data @local-position-data))] + [:g.text-svg {:pointer-events "none"} + [:& svg/text-shape {:shape 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 fad657938..f1179f7e7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -14,11 +14,15 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.cursors :as cur] + [app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]] + [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text.styles :as sts] + [app.main.ui.shapes.text.svg-text :as svg] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.text-editor :as ted] + [app.util.text-svg-position :as utp] [goog.events :as events] [rumext.alpha :as mf]) (:import @@ -233,17 +237,56 @@ ::mf/forward-ref true} [props _] (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape") - clip-id (str "clip-" id)] - [:g.text-editor {:clip-path (str "url(#" clip-id ")")} - [:defs - ;; This clippath will cut the huge foreign object we use to calculate the automatic resize - [:clipPath {:id clip-id} - [:rect {:x x :y y - :width (+ width 8) :height (+ height 8) - :transform (gsh/transform-matrix shape)}]]] - [:foreignObject {:transform (gsh/transform-matrix shape) - :x x :y y - :width (if (#{:auto-width} grow-type) 100000 width) - :height (if (#{:auto-height :auto-width} grow-type) 100000 height)} + transform (str (gsh/transform-matrix shape)) - [:& text-shape-edit-html {:shape shape :key (str id)}]]])) + clip-id (str "clip-" id) + + shape-ref (mf/use-ref nil) + local-position-data (mf/use-state nil) + + handle-change-foreign-object + (mf/use-callback + (fn [node] + (when node + (mf/set-ref-val! shape-ref node) + (let [position-data (utp/calc-position-data node)] + (reset! local-position-data position-data))))) + + handle-interaction + (mf/use-callback + (fn [] + (handle-change-foreign-object (mf/ref-val shape-ref)))) + + on-change-node (use-mutable-observer handle-change-foreign-object)] + + (mf/use-effect + (mf/use-callback handle-interaction) + (fn [] + (let [keys [(events/listen js/document EventType.KEYUP handle-interaction) + (events/listen js/document EventType.KEYDOWN handle-interaction) + (events/listen js/document EventType.MOUSEDOWN handle-interaction)]] + #(doseq [key keys] + (events/unlistenByKey key))))) + [:* + [:> shape-container {:shape shape + :pointer-events "none"} + [:& svg/text-shape {:shape (cond-> shape + (some? @local-position-data) + (assoc :position-data @local-position-data))}]] + + [:g.text-editor {:clip-path (str "url(#" clip-id ")") + :ref on-change-node + :key (str "editor-" id)} + [:defs + ;; This clippath will cut the huge foreign object we use to calculate the automatic resize + [:clipPath {:id clip-id} + [:rect {:x x :y y + :width (+ width 8) :height (+ height 8) + :transform transform}]]] + + [:foreignObject {:transform transform + :x x :y y + :width (if (#{:auto-width} grow-type) 100000 width) + :height (if (#{:auto-height :auto-width} grow-type) 100000 height)} + + [:& text-shape-edit-html {:shape shape :key (str id)}]]]])) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index ffea979c1..6f30c6864 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -8,6 +8,8 @@ (:require [app.common.data :as d] [app.common.geom.point :as gpt] + [app.common.transit :as transit] + [app.main.store :as st] [app.util.dom :as dom] [app.util.globals :as global])) @@ -79,7 +81,7 @@ :width (- (:x p2) (:x p1)) :height (- (:y p2) (:y p1))))) - text-nodes (dom/query-all base-node ".text-node")] + text-nodes (dom/query-all base-node ".text-node, span[data-text]")] (->> text-nodes (mapcat @@ -90,3 +92,34 @@ (map #(update % :position translate-rect)))))) + +(defn calc-position-data + [base-node] + (let [viewport (dom/get-element "render") + zoom (get-in @st/state [:workspace-local :zoom]) + text-data (calc-text-node-positions base-node viewport zoom)] + (->> text-data + (map (fn [{:keys [node position text]}] + (let [{:keys [x y width height]} position + rtl? (= "rtl" (.-dir (.-parentElement ^js node))) + styles (js/getComputedStyle ^js node) + get (fn [prop] + (let [value (.getPropertyValue styles prop)] + (when (and value (not= value "")) + value)))] + (d/without-nils + {:rtl? rtl? + :x (if rtl? (+ x width) x) + :y (+ y height) + :width width + :height height + :font-family (str (get "font-family")) + :font-size (str (get "font-size")) + :font-weight (str (get "font-weight")) + :text-transform (str (get "text-transform")) + :text-decoration (str (get "text-decoration")) + :font-style (str (get "font-style")) + :fill-color (or (get "--fill-color") "#000000") + :fill-color-gradient (transit/decode-str (get "--fill-color-gradient")) + :fill-opacity (d/parse-double (or (get "--fill-opacity") "1")) + :text text})))))))