From a411cbc640888122a04bed81b46b1eb57bb645e7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 14 Feb 2022 12:05:07 +0100 Subject: [PATCH 01/16] :sparkles: Initial SVG text support --- .../app/common/geom/shapes/transforms.cljc | 12 +- exporter/src/app/renderer/svg.cljs | 6 +- .../src/app/main/data/workspace/texts.cljs | 6 +- .../app/main/data/workspace/transforms.cljs | 1 + frontend/src/app/main/ui/render.cljs | 9 +- frontend/src/app/main/ui/shapes/text.cljs | 213 +---------------- .../src/app/main/ui/shapes/text/fo_text.cljs | 218 ++++++++++++++++++ .../src/app/main/ui/shapes/text/svg_text.cljs | 45 ++++ .../app/main/ui/workspace/shapes/frame.cljs | 10 +- .../app/main/ui/workspace/shapes/text.cljs | 72 ++++-- frontend/src/app/util/dom.cljs | 10 +- frontend/src/app/util/text_svg_position.cljs | 92 ++++++++ 12 files changed, 465 insertions(+), 229 deletions(-) create mode 100644 frontend/src/app/main/ui/shapes/text/fo_text.cljs create mode 100644 frontend/src/app/main/ui/shapes/text/svg_text.cljs create mode 100644 frontend/src/app/util/text_svg_position.cljs diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index fa29f33b6..a63623322 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -39,6 +39,14 @@ (->> points (mapv #(gpt/add % move-vec)))) +(defn move-position-data + [position-data dx dy] + + (->> position-data + (map #(-> % + (update :x + dx) + (update :y + dy))))) + (defn move "Move the shape relatively to its current position applying the provided delta." @@ -52,6 +60,7 @@ (update :points move-points move-vec) (d/update-when :x + dx) (d/update-when :y + dy) + (d/update-when :position-data move-position-data dx dy) (cond-> (= :bool type) (update :bool-content gpa/move-content move-vec)) (cond-> (= :path type) (update :content gpa/move-content move-vec))))) @@ -533,7 +542,8 @@ :else (let [shape (apply-displacement shape) - modifiers (:modifiers shape)] + modifiers (:modifiers shape) + shape (cond-> shape (= :text (:type shape)) (assoc :dirty? true))] (cond-> shape (not (empty-modifiers? modifiers)) (-> (set-flip modifiers) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 894d77c4d..45b0afc06 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -311,7 +311,11 @@ xmldata (bw/eval! dom (fn [elem] (.-outerHTML ^js elem))) nodes (process-text-nodes page) nodes (d/index-by :id nodes) - result (replace-text-nodes xmldata nodes)] + result (replace-text-nodes xmldata nodes) + + ;; SVG standard don't allow the entity nbsp.   is equivalent but + ;; compatible with SVG + result (str/replace result " " " ")] ;; (println "------- ORIGIN:") ;; (cljs.pprint/pprint (xml->clj xmldata)) ;; (println "------- RESULT:") diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 061130b15..0c0f79228 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -65,7 +65,7 @@ (when (and (not= content (:content shape)) (some? (:current-page-id state))) (rx/of - (dch/update-shapes [id] #(assoc % :content content)) + (dch/update-shapes [id] #(assoc % :content content :dirty? true)) (dwu/commit-undo-transaction))))) (when (some? id) @@ -149,7 +149,9 @@ [shape pred-fn merge-fn attrs] (let [merge-attrs #(merge-fn % attrs) transform #(txt/transform-nodes pred-fn merge-attrs %)] - (update shape :content transform))) + (-> shape + (update :content transform) + (assoc :dirty? true)))) (defn update-root-attrs [{:keys [id attrs]}] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index ddc4f5686..699bd919d 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -185,6 +185,7 @@ :transform :transform-inverse :rotation + :dirty? :flip-x :flip-y]}) (clear-local-transform) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 0db2dfb00..b5dc467a1 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -20,6 +20,7 @@ [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.shape :refer [shape-container]] + [app.main.ui.shapes.text.fontfaces :as ff] [app.util.dom :as dom] [beicon.core :as rx] [cuerdas.core :as str] @@ -72,6 +73,8 @@ (:hide-fill-on-export object) (assoc :fills [])) + all-children (cph/get-children objects object-id) + {:keys [x y width height] :as bs} (calc-bounds object objects) [_ _ width height :as coords] (->> [x y width height] (map #(* % zoom))) @@ -92,7 +95,9 @@ text-shapes (->> objects (filter (fn [[_ shape]] (= :text (:type shape)))) - (mapv second))] + (mapv second)) + + render-texts? (and render-texts? (some #(nil? (:position-data %)) text-shapes))] (mf/with-effect [width height] (dom/set-page-style {:size (str (mth/ceil width) "px " @@ -110,6 +115,8 @@ ;; https://bugs.chromium.org/p/chromium/issues/detail?id=1244560#c5 :style {:-webkit-print-color-adjust :exact}} + [:& ff/fontfaces-style {:shapes all-children}] + (case (:type object) :frame [:& frame-wrapper {:shape object :view-box vbox}] :group [:> shape-container {:shape object} diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 77ddea023..6da9c4642 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -6,213 +6,16 @@ (ns app.main.ui.shapes.text (:require - [app.common.colors :as clr] - [app.common.data :as d] - [app.common.geom.shapes :as geom] - [app.main.ui.context :as muc] - [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.text.styles :as sts] - [app.util.color :as uc] + [app.main.ui.shapes.text.fo-text :as fo] + [app.main.ui.shapes.text.svg-text :as svg] [app.util.object :as obj] - [cuerdas.core :as str] [rumext.alpha :as mf])) -(mf/defc render-text - {::mf/wrap-props false} - [props] - (let [node (obj/get props "node") - text (:text node) - style (sts/generate-text-styles node)] - [:span.text-node {:style style} - (if (= text "") "\u00A0" text)])) - -(mf/defc render-root - {::mf/wrap-props false} - [props] - (let [node (obj/get props "node") - children (obj/get props "children") - shape (obj/get props "shape") - style (sts/generate-root-styles shape node)] - [:div.root.rich-text - {:style style - :xmlns "http://www.w3.org/1999/xhtml"} - children])) - -(mf/defc render-paragraph-set - {::mf/wrap-props false} - [props] - (let [children (obj/get props "children") - shape (obj/get props "shape") - style (sts/generate-paragraph-set-styles shape)] - [:div.paragraph-set {:style style} children])) - -(mf/defc render-paragraph - {::mf/wrap-props false} - [props] - (let [node (obj/get props "node") - shape (obj/get props "shape") - children (obj/get props "children") - style (sts/generate-paragraph-styles shape node) - dir (:text-direction node "auto")] - [:p.paragraph {:style style :dir dir} children])) - -;; -- Text nodes -(mf/defc render-node - {::mf/wrap-props false} - [props] - (let [{:keys [type text children] :as node} (obj/get props "node")] - (if (string? text) - [:> render-text props] - (let [component (case type - "root" render-root - "paragraph-set" render-paragraph-set - "paragraph" render-paragraph - nil)] - (when component - [:> component props - (for [[index node] (d/enumerate children)] - (let [props (-> (obj/clone props) - (obj/set! "node" node) - (obj/set! "index" index) - (obj/set! "key" index))] - [:> render-node props]))]))))) - -(defn- next-color - "Given a set of colors try to get a color not yet used" - [colors] - (assert (set? colors)) - (loop [current-rgb [0 0 0]] - (let [current-hex (uc/rgb->hex current-rgb)] - (if (contains? colors current-hex) - (recur (uc/next-rgb current-rgb)) - current-hex)))) - -(defn- remap-colors - "Returns a new content replacing the original colors by their mapped 'simple color'" - [content color-mapping] - - (cond-> content - (and (:fill-opacity content) (< (:fill-opacity content) 1.0)) - (-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)])) - (assoc :fill-opacity 1.0)) - - (some? (:fill-color-gradient content)) - (-> (assoc :fill-color (get color-mapping (:fill-color-gradient content))) - (assoc :fill-opacity 1.0) - (dissoc :fill-color-gradient)) - - (contains? content :children) - (update :children #(mapv (fn [node] (remap-colors node color-mapping)) %)))) - -(defn- fill->color - "Given a content node returns the information about that node fill color" - [{:keys [fill-color fill-opacity fill-color-gradient]}] - - (cond - (some? fill-color-gradient) - {:type :gradient - :gradient fill-color-gradient} - - (and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1)) - {:type :transparent - :hex fill-color - :opacity fill-opacity} - - (string? fill-color) - {:type :solid - :hex fill-color - :map-to fill-color})) - -(defn- retrieve-colors - "Given a text shape returns a triple with the values: - - colors used as fills - - a mapping from simple solid colors to complex ones (transparents/gradients) - - the inverse of the previous mapping (to restore the value in the SVG)" - [shape] - (let [color-data - (->> (:content shape) - (tree-seq map? :children) - (map fill->color) - (filter some?)) - - colors (->> color-data - (into #{clr/black} - (comp (filter #(= :solid (:type %))) - (map :hex)))) - - [colors color-data] - (loop [colors colors - head (first color-data) - tail (rest color-data) - result []] - - (if (nil? head) - [colors result] - - (if (= :solid (:type head)) - (recur colors - (first tail) - (rest tail) - (conj result head)) - - (let [next-color (next-color colors) - head (assoc head :map-to next-color) - colors (conj colors next-color)] - (recur colors - (first tail) - (rest tail) - (conj result head)))))) - - color-mapping-inverse - (->> color-data - (remove #(= :solid (:type %))) - (group-by :map-to) - (d/mapm #(first %2))) - - color-mapping - (merge - (->> color-data - (filter #(= :transparent (:type %))) - (map #(vector [(:hex %) (:opacity %)] (:map-to %))) - (into {})) - - (->> color-data - (filter #(= :gradient (:type %))) - (map #(vector (:gradient %) (:map-to %))) - (into {})))] - - [colors color-mapping color-mapping-inverse])) - (mf/defc text-shape - {::mf/wrap-props false - ::mf/forward-ref true} - [props ref] - (let [{:keys [id x y width height content] :as shape} (obj/get props "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) - [colors color-mapping color-mapping-inverse] (retrieve-colors shape) + {::mf/wrap-props false} + [props] - plain-colors? (mf/use-ctx muc/text-plain-colors-ctx) - - content (cond-> content - 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} - ;; 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; - -webkit-background-clip: text;" ] - [:& render-node {:index 0 - :shape shape - :node content}]])) + (let [{:keys [position-data]} (obj/get props "shape")] + (if (some? position-data) + [:> svg/text-shape props] + [:> fo/text-shape props]))) diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs new file mode 100644 index 000000000..7c88c8b69 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -0,0 +1,218 @@ +;; 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.shapes.text.fo-text + (:require + [app.common.colors :as clr] + [app.common.data :as d] + [app.common.geom.shapes :as geom] + [app.main.ui.context :as muc] + [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.text.styles :as sts] + [app.util.color :as uc] + [app.util.object :as obj] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + +(mf/defc render-text + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + text (:text node) + style (sts/generate-text-styles node)] + [:span.text-node {:style style} + (if (= text "") "\u00A0" text)])) + +(mf/defc render-root + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + children (obj/get props "children") + shape (obj/get props "shape") + style (sts/generate-root-styles shape node)] + [:div.root.rich-text + {:style style + :xmlns "http://www.w3.org/1999/xhtml"} + children])) + +(mf/defc render-paragraph-set + {::mf/wrap-props false} + [props] + (let [children (obj/get props "children") + shape (obj/get props "shape") + style (sts/generate-paragraph-set-styles shape)] + [:div.paragraph-set {:style style} children])) + +(mf/defc render-paragraph + {::mf/wrap-props false} + [props] + (let [node (obj/get props "node") + shape (obj/get props "shape") + children (obj/get props "children") + style (sts/generate-paragraph-styles shape node) + dir (:text-direction node "auto")] + [:p.paragraph {:style style :dir dir} children])) + +;; -- Text nodes +(mf/defc render-node + {::mf/wrap-props false} + [props] + (let [{:keys [type text children] :as node} (obj/get props "node")] + (if (string? text) + [:> render-text props] + (let [component (case type + "root" render-root + "paragraph-set" render-paragraph-set + "paragraph" render-paragraph + nil)] + (when component + [:> component props + (for [[index node] (d/enumerate children)] + (let [props (-> (obj/clone props) + (obj/set! "node" node) + (obj/set! "index" index) + (obj/set! "key" index))] + [:> render-node props]))]))))) + +(defn- next-color + "Given a set of colors try to get a color not yet used" + [colors] + (assert (set? colors)) + (loop [current-rgb [0 0 0]] + (let [current-hex (uc/rgb->hex current-rgb)] + (if (contains? colors current-hex) + (recur (uc/next-rgb current-rgb)) + current-hex)))) + +(defn- remap-colors + "Returns a new content replacing the original colors by their mapped 'simple color'" + [content color-mapping] + + (cond-> content + (and (:fill-opacity content) (< (:fill-opacity content) 1.0)) + (-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)])) + (assoc :fill-opacity 1.0)) + + (some? (:fill-color-gradient content)) + (-> (assoc :fill-color (get color-mapping (:fill-color-gradient content))) + (assoc :fill-opacity 1.0) + (dissoc :fill-color-gradient)) + + (contains? content :children) + (update :children #(mapv (fn [node] (remap-colors node color-mapping)) %)))) + +(defn- fill->color + "Given a content node returns the information about that node fill color" + [{:keys [fill-color fill-opacity fill-color-gradient]}] + + (cond + (some? fill-color-gradient) + {:type :gradient + :gradient fill-color-gradient} + + (and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1)) + {:type :transparent + :hex fill-color + :opacity fill-opacity} + + (string? fill-color) + {:type :solid + :hex fill-color + :map-to fill-color})) + +(defn- retrieve-colors + "Given a text shape returns a triple with the values: + - colors used as fills + - a mapping from simple solid colors to complex ones (transparents/gradients) + - the inverse of the previous mapping (to restore the value in the SVG)" + [shape] + (let [color-data + (->> (:content shape) + (tree-seq map? :children) + (map fill->color) + (filter some?)) + + colors (->> color-data + (into #{clr/black} + (comp (filter #(= :solid (:type %))) + (map :hex)))) + + [colors color-data] + (loop [colors colors + head (first color-data) + tail (rest color-data) + result []] + + (if (nil? head) + [colors result] + + (if (= :solid (:type head)) + (recur colors + (first tail) + (rest tail) + (conj result head)) + + (let [next-color (next-color colors) + head (assoc head :map-to next-color) + colors (conj colors next-color)] + (recur colors + (first tail) + (rest tail) + (conj result head)))))) + + color-mapping-inverse + (->> color-data + (remove #(= :solid (:type %))) + (group-by :map-to) + (d/mapm #(first %2))) + + color-mapping + (merge + (->> color-data + (filter #(= :transparent (:type %))) + (map #(vector [(:hex %) (:opacity %)] (:map-to %))) + (into {})) + + (->> color-data + (filter #(= :gradient (:type %))) + (map #(vector (:gradient %) (:map-to %))) + (into {})))] + + [colors color-mapping color-mapping-inverse])) + +(mf/defc text-shape + {::mf/wrap-props false + ::mf/forward-ref true} + [props ref] + (let [{:keys [id x y width height content] :as shape} (obj/get props "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) + [colors color-mapping color-mapping-inverse] (retrieve-colors shape) + + plain-colors? (mf/use-ctx muc/text-plain-colors-ctx) + + content (cond-> content + 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} + ;; 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; + -webkit-background-clip: text;" ] + [:& render-node {:index 0 + :shape shape + :node content}]])) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs new file mode 100644 index 000000000..9863c99ff --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -0,0 +1,45 @@ +;; 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.shapes.text.svg-text + (:require + [app.common.geom.matrix :as gmt] + [app.common.geom.shapes :as gsh] + [app.main.store :as st] + [app.util.object :as obj] + [rumext.alpha :as mf])) + +(mf/defc text-shape + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [props] + + (let [{:keys [x y width height position-data] :as shape} (obj/get props "shape") + zoom (or (get-in @st/state [:workspace-local :zoom]) 1)] + [:text {:x x + :y y + :width width + :height height + :dominant-baseline "ideographic" + :transform (gsh/transform-matrix shape) + } + (for [data position-data] + [:tspan {:x (:x data) + :y (:y data) + :transform (:transform-inverse shape (gmt/matrix)) + :style {:fill "black" + :fill-opacity 1 + :stroke "red" + :stroke-width (/ 0.5 zoom) + :font-family (:font-family data) + :font-size (:font-size data) + :font-weight (:font-weight data) + :text-transform (:text-transform data) + :text-decoration (:text-decoration data) + :font-style (:font-style data) + :direction (if (:rtl? data) "rtl" "ltr") + :white-space "pre"}} + (:text data)])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index def968464..6ccb13636 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -108,8 +108,16 @@ (-> (cph/get-children objects (:id shape)) (hooks/use-equal-memo)) + all-svg-text? + (mf/use-memo + (mf/deps all-children) + (fn [] + (->> all-children + (filter #(= :text (:type %))) + (every? #(some? (:position-data %)))))) + show-thumbnail? - (and thumbnail? (some? (:thumbnail shape)))] + (and thumbnail? (some? (:thumbnail shape)) all-svg-text?)] [:g.frame-wrapper {:display (when (:hidden shape) "none")} [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 09fc36698..e9563a175 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -8,14 +8,17 @@ (:require [app.common.logging :as log] [app.common.math :as mth] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text :as text] + [app.main.ui.shapes.text.fo-text :as fo] + [app.main.ui.shapes.text.svg-text :as svg] [app.util.dom :as dom] [app.util.object :as obj] [app.util.text-editor :as ted] + [app.util.text-svg-position :as utp] [app.util.timers :as timers] [app.util.webapi :as wapi] [beicon.core :as rx] @@ -29,7 +32,7 @@ (mf/defc text-static-content [{:keys [shape]}] - [:& text/text-shape {:shape shape + [:& fo/text-shape {:shape shape :grow-type (:grow-type shape)}]) (defn- update-with-current-editor-state @@ -99,24 +102,67 @@ (mf/use-effect (fn [] #(mf/set-ref-val! mnt false))) - [:& text/text-shape {:ref text-ref-cb :shape shape :grow-type (:grow-type shape)}])) + [:& fo/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] :as shape} (unchecked-get props "shape") + (let [{:keys [id dirty?] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) - edition? (mf/deref edition-ref)] + edition? (mf/deref edition-ref) + shape-ref (mf/use-ref nil)] + + (mf/use-layout-effect + (mf/deps dirty?) + (fn [] + (when (and (or dirty? (not (:position-data shape))) (some? id)) + (let [base-node (mf/ref-val shape-ref) + viewport (dom/get-element "render") + zoom (get-in @st/state [:workspace-local :zoom]) + text-data (utp/calc-text-node-positions base-node viewport zoom) + position-data + (->> 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)] + {: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")) + :text text}))))] + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (dissoc :dirty?) + (assoc :position-data position-data))))))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly - [:g.text-shape {:opacity (when edition? 0) - :pointer-events "none"} + [:* + [:g.text-shape {:ref shape-ref + :opacity (when (or edition? (some? (:position-data shape))) 0) + :pointer-events "none"} - ;; The `:key` prop here is mandatory because the - ;; text-resize-content breaks a hooks rule and we can't reuse - ;; the component if the edition flag changes. - [:& text-resize-content {:shape shape - :edition? edition? - :key (str id edition?)}]]])) + ;; The `:key` prop here is mandatory because the + ;; text-resize-content breaks a hooks rule and we can't reuse + ;; the component if the edition flag changes. + [:& text-resize-content {:shape (cond-> shape + (:position-data shape) + (dissoc :transform :transform-inverse)) + :edition? edition? + :key (str id edition?)}]] + + [:g {:opacity (when edition? 0) + :pointer-events "none"} + (when (some? (:position-data shape)) + [:& svg/text-shape {:shape shape}])]]])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 76a7500cf..d35c8c3ae 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -264,11 +264,11 @@ :height (.-height ^js rect)})) (defn bounding-rect->rect - [{:keys [left top width height]}] - {:x left - :y top - :width width - :height height}) + [rect] + {:x (or (.-left rect) (:left rect)) + :y (or (.-top rect) (:top rect)) + :width (or (.-width rect) (:width rect)) + :height (or (.-height rect) (:height rect))}) (defn get-window-size [] diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs new file mode 100644 index 000000000..ffea979c1 --- /dev/null +++ b/frontend/src/app/util/text_svg_position.cljs @@ -0,0 +1,92 @@ +;; 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.util.text-svg-position + (:require + [app.common.data :as d] + [app.common.geom.point :as gpt] + [app.util.dom :as dom] + [app.util.globals :as global])) + +(defn get-range-rects + "Retrieve the rectangles that cover the selection given by a `node` adn + the start and end index `start-i`, `end-i`" + [^js node start-i end-i] + (let [^js range (.createRange global/document)] + (.setStart range node start-i) + (.setEnd range node end-i) + (.getClientRects range))) + +(defn parse-text-nodes + "Given a text node retrieves the rectangles for everyone of its paragraphs and its text." + [parent-node rtl? text-node] + + (let [content (.-textContent text-node) + text-size (.-length content)] + + (loop [from-i 0 + to-i 0 + current "" + result []] + (if (>= to-i text-size) + (let [rects (get-range-rects text-node from-i to-i) + entry {:node parent-node + :position (dom/bounding-rect->rect (first rects)) + :text current}] + ;; We need to add the last element not closed yet + (conj result entry)) + + (let [rects (get-range-rects text-node from-i (inc to-i))] + ;; If the rects increase means we're in a new paragraph + (if (> (.-length rects) 1) + (let [entry {:node parent-node + :position (dom/bounding-rect->rect (if rtl? (second rects) (first rects))) + :text current}] + (recur to-i to-i "" (conj result entry))) + (recur from-i (inc to-i) (str current (nth content to-i)) result))))))) + + +(defn calc-text-node-positions + [base-node viewport zoom] + + (when (some? viewport) + (let [translate-point + (fn [pt] + (let [vbox (.. ^js viewport -viewBox -baseVal) + brect (dom/get-bounding-rect viewport) + brect (gpt/point (d/parse-integer (:left brect)) + (d/parse-integer (:top brect))) + box (gpt/point (.-x vbox) (.-y vbox)) + zoom (gpt/point zoom)] + + (-> (gpt/subtract pt brect) + (gpt/divide zoom) + (gpt/add box)))) + + translate-rect + (fn [{:keys [x y width height] :as rect}] + (let [p1 (-> (gpt/point x y) + (translate-point)) + + p2 (-> (gpt/point (+ x width) (+ y height)) + (translate-point))] + (assoc rect + :x (:x p1) + :y (:y p1) + :width (- (:x p2) (:x p1)) + :height (- (:y p2) (:y p1))))) + + text-nodes (dom/query-all base-node ".text-node")] + + (->> text-nodes + (mapcat + (fn [parent-node] + (let [rtl? (= "rtl" (.-dir (.-parentElement parent-node)))] + (->> (.-childNodes parent-node) + (mapcat #(parse-text-nodes parent-node rtl? %)))))) + (map #(update % :position translate-rect)))))) + + From 1c2785f34ec73c29b512484a935fff12e10d1b05 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 14 Feb 2022 17:13:32 +0100 Subject: [PATCH 02/16] :sparkles: Adds borders to SVG texts --- frontend/src/app/main/ui/shapes/attrs.cljs | 5 +- .../src/app/main/ui/shapes/gradients.cljs | 8 +-- .../src/app/main/ui/shapes/text/fo_text.cljs | 8 ++- .../src/app/main/ui/shapes/text/svg_text.cljs | 68 +++++++++++-------- .../app/main/ui/workspace/shapes/text.cljs | 34 ++++++---- .../sidebar/options/shapes/text.cljs | 7 ++ .../src/app/main/ui/workspace/viewport.cljs | 3 +- 7 files changed, 82 insertions(+), 51 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 1da44c40f..51e84ab26 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -189,9 +189,8 @@ (let [svg-defs (:svg-defs shape {}) svg-attrs (:svg-attrs shape {}) - [svg-attrs svg-styles] (mf/use-memo - (mf/deps render-id svg-defs svg-attrs) - #(extract-svg-attrs render-id svg-defs svg-attrs)) + [svg-attrs svg-styles] + (extract-svg-attrs render-id svg-defs svg-attrs) styles (-> (obj/get props "style" (obj/new)) (obj/merge! svg-styles) diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index bd9bca876..8396ae47b 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -97,10 +97,10 @@ (mf/defc gradient {::mf/wrap-props false} [props] - (let [attr (obj/get props "attr") - shape (obj/get props "shape") - render-id (mf/use-ctx muc/render-ctx) - id (str (name attr) "_" render-id) + (let [attr (obj/get props "attr") + shape (obj/get props "shape") + id (obj/get props "id") + id (or id (str (name attr) "_" (mf/use-ctx muc/render-ctx))) gradient (get shape attr) gradient-props #js {:id id :gradient gradient 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 7c88c8b69..ae9929b7d 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -9,6 +9,7 @@ [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] @@ -23,7 +24,12 @@ (let [node (obj/get props "node") text (:text node) style (sts/generate-text-styles node)] - [:span.text-node {:style style} + [: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)} (if (= text "") "\u00A0" text)])) (mf/defc render-root 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 9863c99ff..3f6ff5313 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -6,40 +6,52 @@ (ns app.main.ui.shapes.text.svg-text (:require - [app.common.geom.matrix :as gmt] + [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.main.store :as st] + [app.main.ui.context :as muc] + [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [rumext.alpha :as mf])) +(def fill-attrs [:fill-color :fill-color-gradient :fill-opacity]) + (mf/defc text-shape {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] - (let [{:keys [x y width height position-data] :as shape} (obj/get props "shape") - zoom (or (get-in @st/state [:workspace-local :zoom]) 1)] - [:text {:x x - :y y - :width width - :height height - :dominant-baseline "ideographic" - :transform (gsh/transform-matrix shape) - } - (for [data position-data] - [:tspan {:x (:x data) - :y (:y data) - :transform (:transform-inverse shape (gmt/matrix)) - :style {:fill "black" - :fill-opacity 1 - :stroke "red" - :stroke-width (/ 0.5 zoom) - :font-family (:font-family data) - :font-size (:font-size data) - :font-weight (:font-weight data) - :text-transform (:text-transform data) - :text-decoration (:text-decoration data) - :font-style (:font-style data) - :direction (if (:rtl? data) "rtl" "ltr") - :white-space "pre"}} - (:text data)])])) + (let [render-id (mf/use-ctx muc/render-ctx) + {:keys [position-data] :as shape} (obj/get props "shape") + group-props (-> #js {:transform (gsh/transform-matrix shape)} + (attrs/add-style-attrs shape render-id)) + get-gradient-id + (fn [index] + (str render-id "_" (:id shape) "_" index))] + [:* + ;; Definition of gradients for partial elements + (when (d/seek :fill-color-gradient position-data) + [:defs + (for [[index data] (d/enumerate position-data)] + (when (some? (:fill-color-gradient data)) + [:& grad/gradient {:id (str "fill-color-gradient_" (get-gradient-id index)) + :attr :fill-color-gradient + :shape data}]))]) + + [:& shape-custom-stroke {:shape shape} + [:> :g group-props + (for [[index data] (d/enumerate position-data)] + (let [props (-> #js {:x (:x data) + :y (:y data) + :dominant-baseline "ideographic" + :style (-> #js {:fontFamily (:font-family data) + :fontSize (:font-size data) + :fontWeight (:font-weight data) + :textTransform (:text-transform data) + :textDecoration (:text-decoration data) + :fontStyle (:font-style data) + :direction (if (:rtl? data) "rtl" "ltr") + :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 e9563a175..741073d0d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,8 +6,10 @@ (ns app.main.ui.workspace.shapes.text (:require + [app.common.data :as d] [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] @@ -126,18 +128,22 @@ (let [{:keys [x y width height]} position rtl? (= "rtl" (.-dir (.-parentElement ^js node))) styles (.computedStyleMap ^js node)] - {: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")) - :text text}))))] + (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})))))] (st/emit! (dch/update-shapes [id] (fn [shape] @@ -162,7 +168,7 @@ :edition? edition? :key (str id edition?)}]] - [:g {:opacity (when edition? 0) - :pointer-events "none"} + [:g.text-svg {:opacity (when edition? 0) + :pointer-events "none"} (when (some? (:position-data shape)) [:& svg/text-shape {:shape shape}])]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index 804703cd6..fa1990b2a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -16,6 +16,7 @@ [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] [app.main.ui.workspace.sidebar.options.menus.measures :refer [measure-attrs measures-menu]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] + [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu text-fill-attrs root-attrs paragraph-attrs text-attrs]] [rumext.alpha :as mf])) @@ -43,6 +44,8 @@ (:fill fill-values) (assoc :fill-color (:fill fill-values)) (:opacity fill-values) (assoc :fill-opacity (:fill fill-values))) + stroke-values (select-keys shape stroke-attrs) + text-values (d/merge (select-keys shape [:grow-type]) (select-keys shape fill-attrs) @@ -79,6 +82,10 @@ :values fill-values :disable-remove? true}] + [:& stroke-menu {:ids ids + :type type + :values stroke-values}] + [:& shadow-menu {:ids ids :values (select-keys shape [:shadow])}] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index ffbdc7d39..fd6fc1afd 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -206,7 +206,8 @@ :style {:background-color background :pointer-events "none"}} - [:& use/export-page {:options options}] + (when (debug? :show-export-metadata) + [:& use/export-page {:options options}]) [:& (mf/provider use/include-metadata-ctx) {:value (debug? :show-export-metadata)} [:& (mf/provider embed/context) {:value true} From 18dded1a0093b92d647c8379b9541e4a03fcf723 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 15 Feb 2022 18:22:13 +0100 Subject: [PATCH 03/16] :sparkles: Fix editor and bounds for new texts --- common/src/app/common/geom/matrix.cljc | 17 ++ .../src/app/main/ui/shapes/text/styles.cljs | 12 +- .../src/app/main/ui/shapes/text/svg_text.cljs | 16 +- .../app/main/ui/workspace/shapes/text.cljs | 157 +++++++++++++----- .../app/main/ui/workspace/viewport/utils.cljs | 24 ++- 5 files changed, 165 insertions(+), 61 deletions(-) diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index 88ba2ed5d..ed9d8bf0f 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -190,3 +190,20 @@ (multiply mtx) (translate (gpt/negate pt))) mtx)) + +(defn determinant + "Determinant for the affinity transform" + [{:keys [a b c d _ _]}] + (- (* a d) (* c b))) + +(defn inverse + "Gets the inverse of the affinity transform `mtx`" + [{:keys [a b c d e f] :as mtx}] + (let [det (determinant mtx) + a' (/ d det) + b' (/ (- b) det) + c' (/ (- c) det) + d' (/ a det) + e' (/ (- (* c f) (* d e)) det) + f' (/ (- (* b e) (* a f)) det)] + (Matrix. a' b' c' d' e' f'))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index fe7efceef..2af461501 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -84,16 +84,18 @@ base #js {:textDecoration text-decoration :textTransform text-transform :lineHeight (or line-height "inherit") - :color text-color}] + :color text-color + :caretColor "black"}] (when-let [gradient (:fill-color-gradient data)] (let [text-color (-> (update gradient :type keyword) (uc/gradient->css))] (-> base - (obj/set! "--text-color" text-color) - (obj/set! "backgroundImage" "var(--text-color)") - (obj/set! "WebkitTextFillColor" "transparent") - (obj/set! "WebkitBackgroundClip" "text")))) + (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")))) (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 3f6ff5313..607860b1a 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -23,9 +23,11 @@ [props] (let [render-id (mf/use-ctx muc/render-ctx) - {:keys [position-data] :as shape} (obj/get props "shape") - group-props (-> #js {:transform (gsh/transform-matrix shape)} - (attrs/add-style-attrs shape render-id)) + {:keys [id x y width height position-data] :as shape} (obj/get props "shape") + clip-id (str "clip-text" id "_" render-id) + group-props (-> #js {:transform (gsh/transform-matrix shape) + :clipPath (str "url(#" clip-id ")")} + (attrs/add-style-attrs shape render-id)) get-gradient-id (fn [index] (str render-id "_" (:id shape) "_" index))] @@ -41,10 +43,16 @@ [:& shape-custom-stroke {:shape shape} [:> :g group-props + [:defs + [:clipPath {:id clip-id} + [:rect.text-clip + {:x x :y y + :width width :height height + :transform (gsh/transform-matrix shape)}]]] (for [[index data] (d/enumerate position-data)] (let [props (-> #js {:x (:x data) :y (:y data) - :dominant-baseline "ideographic" + :dominantBaseline "ideographic" :style (-> #js {:fontFamily (:font-family data) :fontSize (:font-size data) :fontWeight (:font-weight data) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 741073d0d..cefb85a50 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,7 +6,10 @@ (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] @@ -19,6 +22,7 @@ [app.main.ui.shapes.text.svg-text :as svg] [app.util.dom :as dom] [app.util.object :as obj] + [app.util.svg :as usvg] [app.util.text-editor :as ted] [app.util.text-svg-position :as utp] [app.util.timers :as timers] @@ -40,12 +44,18 @@ (defn- update-with-current-editor-state [{:keys [id] :as shape}] (let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state)) - editor-state (mf/deref editor-state-ref)] + editor-state (mf/deref editor-state-ref) + + content (:content shape) + editor-content + (when editor-state + (-> editor-state + (ted/get-editor-current-content) + (ted/export-content)))] + (cond-> shape - (some? editor-state) - (assoc :content (-> editor-state - (ted/get-editor-current-content) - (ted/export-content)))))) + (some? editor-content) + (assoc :content (attrs/merge content editor-content))))) (mf/defc text-resize-content {::mf/wrap-props false} @@ -106,69 +116,126 @@ [:& 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}))))))) + + + (mf/defc text-wrapper {::mf/wrap-props false} [props] (let [{:keys [id dirty?] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref) - shape-ref (mf/use-ref nil)] + 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) + parent (dom/get-parent node) + parent-transform (dom/get-attribute parent "transform") + node-transform (dom/get-attribute node "transform") + + parent-mtx (usvg/parse-transform parent-transform) + node-mtx (usvg/parse-transform node-transform) + + ;; We need to see what transformation is applied in the DOM to reverse it + ;; before calculating the position data + mtx (-> (gmt/multiply parent-mtx node-mtx) + (gmt/inverse)) + + position-data' + (->> position-data + (mapv #(merge % (-> (select-keys % [:x :y :width :height]) + (gsh/transform-rect mtx)))))] + (reset! local-position-data position-data')))) + + 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))))) (mf/use-layout-effect - (mf/deps dirty?) + (mf/deps id dirty?) (fn [] - (when (and (or dirty? (not (:position-data shape))) (some? id)) - (let [base-node (mf/ref-val shape-ref) - viewport (dom/get-element "render") - zoom (get-in @st/state [:workspace-local :zoom]) - text-data (utp/calc-text-node-positions base-node viewport zoom) - position-data - (->> 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})))))] - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (dissoc :dirty?) - (assoc :position-data position-data))))))))) + (let [node (mf/ref-val shape-ref) + position-data (calc-position-data node)] + (reset! local-position-data nil) + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (dissoc :dirty?) + (assoc :position-data position-data)))))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly [:* - [:g.text-shape {:ref shape-ref + [:g.text-shape {:ref on-change-node :opacity (when (or edition? (some? (:position-data shape))) 0) :pointer-events "none"} ;; The `:key` prop here is mandatory because the ;; text-resize-content breaks a hooks rule and we can't reuse ;; the component if the edition flag changes. - [:& text-resize-content {:shape (cond-> shape - (:position-data shape) - (dissoc :transform :transform-inverse)) + [:& text-resize-content {:shape + (cond-> shape + (:position-data shape) + (dissoc :transform :transform-inverse)) :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 shape}])]]])) + [:& svg/text-shape {:shape (cond-> shape + (some? @local-position-data) + (assoc :position-data @local-position-data))}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 873061070..6e7db9411 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -105,7 +105,9 @@ text? [shape-node (dom/query shape-node "foreignObject") - (dom/query shape-node ".text-shape")] + (dom/query shape-node ".text-shape") + (dom/query shape-node ".text-svg") + (dom/query shape-node ".text-clip")] :else [shape-node]))) @@ -118,18 +120,26 @@ [text-transform text-width text-height] (when (= :text type) - (text-corrected-transform shape transform modifiers))] + (text-corrected-transform shape transform modifiers)) + + text-width (str text-width) + text-height (str text-height)] (doseq [node nodes] (cond - (dom/class? node "text-shape") + (or (dom/class? node "text-shape") (dom/class? node "text-svg")) (when (some? text-transform) (dom/set-attribute node "transform" (str text-transform))) - (= (dom/get-tag-name node) "foreignObject") - (when (and (some? text-width) (some? text-height)) - (dom/set-attribute node "width" text-width) - (dom/set-attribute node "height" text-height)) + (or (= (dom/get-tag-name node) "foreignObject") + (dom/class? node "text-clip")) + (let [cur-width (dom/get-attribute node "width") + cur-height (dom/get-attribute node "height")] + (when (and (some? text-width) (not= cur-width text-width)) + (dom/set-attribute node "width" text-width)) + + (when (and (some? text-height) (not= cur-height text-height)) + (dom/set-attribute node "height" text-height))) (and (some? transform) (some? node)) (dom/set-attribute node "transform" (str transform)))))))) From 6cb6adc134e33fbe6814cacdf2183c4c66f6e83d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 16 Feb 2022 16:18:15 +0100 Subject: [PATCH 04/16] :sparkles: Allows svg text on test edit and creation --- .../app/main/ui/hooks/mutable_observer.cljs | 54 +++++++++++ .../src/app/main/ui/shapes/gradients.cljs | 8 +- .../src/app/main/ui/shapes/text/fo_text.cljs | 34 ++++--- .../src/app/main/ui/shapes/text/styles.cljs | 24 ++--- .../src/app/main/ui/shapes/text/svg_text.cljs | 7 +- .../app/main/ui/workspace/shapes/text.cljs | 90 +++++-------------- .../main/ui/workspace/shapes/text/editor.cljs | 69 +++++++++++--- frontend/src/app/util/text_svg_position.cljs | 35 +++++++- 8 files changed, 198 insertions(+), 123 deletions(-) create mode 100644 frontend/src/app/main/ui/hooks/mutable_observer.cljs 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}))))))) From d83459f67403b7a9e348fe8bf202389072b0246b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Feb 2022 09:23:08 +0100 Subject: [PATCH 05/16] :sparkle: Change mutation listener --- .../app/main/ui/hooks/mutable_observer.cljs | 12 ++++--- .../app/main/ui/workspace/shapes/text.cljs | 29 +++++++-------- .../main/ui/workspace/shapes/text/editor.cljs | 35 +++++++++---------- frontend/src/app/util/dom.cljs | 9 ++--- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/frontend/src/app/main/ui/hooks/mutable_observer.cljs b/frontend/src/app/main/ui/hooks/mutable_observer.cljs index 42f8b2c1b..f9852fea9 100644 --- a/frontend/src/app/main/ui/hooks/mutable_observer.cljs +++ b/frontend/src/app/main/ui/hooks/mutable_observer.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.hooks.mutable-observer (:require + [app.common.data :as d] [app.common.logging :as log] [rumext.alpha :as mf])) @@ -20,9 +21,12 @@ 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)))) + (fn [mutations] + (let [mutations + (->> mutations + (remove #(= "transform" (.-attributeName ^js %))))] + (when (d/not-empty? mutations) + (on-change (mf/ref-val node-ref)))))) set-node (mf/use-callback @@ -51,4 +55,4 @@ (.disconnect prev-obs) (mf/set-ref-val! prev-obs-ref nil))))) - set-node)) + [node-ref set-node])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index c31e71ad8..b2589c91e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -125,14 +125,13 @@ (let [{:keys [id dirty?] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref) - shape-ref (mf/use-ref nil) local-position-data (mf/use-state nil) handle-change-foreign-object (fn [node] (when (some? node) - (mf/set-ref-val! shape-ref node) + (prn "change!") (let [position-data (utp/calc-position-data node) parent (dom/get-parent node) parent-transform (dom/get-attribute parent "transform") @@ -152,28 +151,30 @@ (gsh/transform-rect mtx)))))] (reset! local-position-data position-data)))) - on-change-node (use-mutable-observer handle-change-foreign-object)] + [shape-ref 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 (utp/calc-position-data node)] - (reset! local-position-data nil) - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (dissoc :dirty?) - (assoc :position-data position-data)))))))) + (let [node (mf/ref-val shape-ref)] + (when (and dirty? (some? node)) + (let [position-data (utp/calc-position-data node)] + (reset! local-position-data nil) + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (dissoc :dirty?) + (assoc :position-data position-data))) + {:save-undo? false}))))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly [:* [:g.text-shape {:ref on-change-node - :opacity (when (or edition? (some? (:position-data shape))) 0) + :opacity (when (or edition? (some? (:position-data shape))) 0.2) :pointer-events "none"} ;; The `:key` prop here is mandatory because the @@ -186,7 +187,7 @@ :edition? edition? :key (str id edition?)}]] - (when (and (not edition?) (or (some? (:position-data shape)) (some? local-position-data))) + (when (and (or (some? (:position-data shape)) (some? local-position-data))) (let [shape (cond-> shape (some? @local-position-data) 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 f1179f7e7..4278a005c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -241,25 +241,24 @@ clip-id (str "clip-" id) - shape-ref (mf/use-ref nil) - local-position-data (mf/use-state 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-change-foreign-object + ;;(mf/use-callback + ;; (fn [node] + ;; (when node + ;; (let [position-data (utp/calc-position-data node)] + ;; (reset! local-position-data position-data))))) + ;; + ;;[shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object) - handle-interaction - (mf/use-callback - (fn [] - (handle-change-foreign-object (mf/ref-val shape-ref)))) + ;;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-effect (mf/use-callback handle-interaction) (fn [] (let [keys [(events/listen js/document EventType.KEYUP handle-interaction) @@ -268,14 +267,14 @@ #(doseq [key keys] (events/unlistenByKey key))))) [:* - [:> shape-container {:shape shape + #_[:> 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 + ;; :ref on-change-node :key (str "editor-" id)} [:defs ;; This clippath will cut the huge foreign object we use to calculate the automatic resize diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index d35c8c3ae..b8af3ed89 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -265,10 +265,11 @@ (defn bounding-rect->rect [rect] - {:x (or (.-left rect) (:left rect)) - :y (or (.-top rect) (:top rect)) - :width (or (.-width rect) (:width rect)) - :height (or (.-height rect) (:height rect))}) + (when (some? rect) + {:x (or (.-left rect) (:left rect)) + :y (or (.-top rect) (:top rect)) + :width (or (.-width rect) (:width rect)) + :height (or (.-height rect) (:height rect))})) (defn get-window-size [] From 618d22d2144ccbb9e52e7ea28a49da3b8b8ff13e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Feb 2022 14:53:46 +0100 Subject: [PATCH 06/16] :sparkles: Changes to text editor --- .../resources/styles/main/partials/texts.scss | 83 +++++++++--------- frontend/src/app/main/ui/cursors.cljs | 2 +- frontend/src/app/main/ui/shapes/attrs.cljs | 48 ++++++----- .../src/app/main/ui/shapes/custom_stroke.cljs | 15 +--- .../src/app/main/ui/shapes/text/styles.cljs | 10 +-- .../src/app/main/ui/shapes/text/svg_text.cljs | 8 +- .../app/main/ui/workspace/shapes/text.cljs | 3 +- .../main/ui/workspace/shapes/text/editor.cljs | 84 +++++++------------ .../src/app/main/ui/workspace/viewport.cljs | 11 ++- 9 files changed, 115 insertions(+), 149 deletions(-) diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index 4b4a2f711..d84442eb1 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -1,62 +1,61 @@ -foreignObject { - .text-editor, - .rich-text { - color: $color-black; - height: 100%; - white-space: pre-wrap; - font-family: sourcesanspro; - div { - line-height: inherit; - user-select: text; - } +.text-editor, +.rich-text { + color: $color-black; + height: 100%; + white-space: pre-wrap; + font-family: sourcesanspro; - span { - line-height: inherit; - } + div { + line-height: inherit; + user-select: text; } - .text-editor { - .DraftEditor-root { - height: 100%; - display: flex; - flex-direction: column; - } - - &.align-top { - .DraftEditor-root { - justify-content: flex-start; - } - } - - &.align-center { - .DraftEditor-root { - justify-content: center; - } - } - - &.align-bottom { - .DraftEditor-root { - justify-content: flex-end; - } - } + span { + line-height: inherit; } +} - .rich-text .paragraphs { +.text-editor { + .DraftEditor-root { height: 100%; display: flex; flex-direction: column; + } - &.align-top { + &.align-top { + .DraftEditor-root { justify-content: flex-start; } + } - &.align-center { + &.align-center { + .DraftEditor-root { justify-content: center; } + } - &.align-bottom { + &.align-bottom { + .DraftEditor-root { justify-content: flex-end; } } } + +.rich-text .paragraphs { + height: 100%; + display: flex; + flex-direction: column; + + &.align-top { + justify-content: flex-start; + } + + &.align-center { + justify-content: center; + } + + &.align-bottom { + justify-content: flex-end; + } +} diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 2d571ed06..144c0e853 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -29,7 +29,6 @@ (def pointer-move (cursor-ref :pointer-move 0 0 10 42)) (def pointer-node (cursor-ref :pointer-node 0 0 10 32)) (def resize-alt (cursor-ref :resize-alt)) -(def text (cursor-ref :text)) (def zoom (cursor-ref :zoom)) (def zoom-in (cursor-ref :zoom-in)) (def zoom-out (cursor-ref :zoom-out)) @@ -40,6 +39,7 @@ (def resize-ns (cursor-fn :resize-h 90)) (def resize-nwse (cursor-fn :resize-h 135)) (def rotate (cursor-fn :rotate 90)) +(def text (cursor-fn :text 0)) ;; (def resize-ew-2 (cursor-fn :resize-h-2 0)) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 51e84ab26..3a30cf7f3 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -80,34 +80,38 @@ "z")})) attrs)) -(defn add-fill [attrs shape render-id index] - (let [ - fill-attrs (cond - (contains? shape :fill-image) - (let [fill-image-id (str "fill-image-" render-id)] - {:fill (str/format "url(#%s)" fill-image-id)}) +(defn add-fill + ([attrs shape render-id] + (add-fill attrs shape render-id 0)) - (contains? shape :fill-color-gradient) - (let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)] - {:fill (str/format "url(#%s)" fill-color-gradient-id)}) + ([attrs shape render-id index] + (let [fill-attrs + (cond + (contains? shape :fill-image) + (let [fill-image-id (str "fill-image-" render-id)] + {:fill (str/format "url(#%s)" fill-image-id)}) - (contains? shape :fill-color) - {:fill (:fill-color shape)} + (contains? shape :fill-color-gradient) + (let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)] + {:fill (str/format "url(#%s)" fill-color-gradient-id)}) - ;; If contains svg-attrs the origin is svg. If it's not svg origin - ;; we setup the default fill as transparent (instead of black) - (and (not (contains? shape :svg-attrs)) - (not (#{:svg-raw :group} (:type shape)))) - {:fill "none"} + (contains? shape :fill-color) + {:fill (:fill-color shape)} - :else - {}) + ;; If contains svg-attrs the origin is svg. If it's not svg origin + ;; we setup the default fill as transparent (instead of black) + (and (not (contains? shape :svg-attrs)) + (not (#{:svg-raw :group} (:type shape)))) + {:fill "none"} - fill-attrs (cond-> fill-attrs - (contains? shape :fill-opacity) - (assoc :fillOpacity (:fill-opacity shape)))] + :else + {}) - (obj/merge! attrs (clj->js fill-attrs)))) + fill-attrs (cond-> fill-attrs + (contains? shape :fill-opacity) + (assoc :fillOpacity (:fill-opacity shape)))] + + (obj/merge! attrs (clj->js fill-attrs))))) (defn add-stroke [attrs shape render-id] (let [stroke-style (:stroke-style shape :none) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index b88e72722..04e688a6f 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -38,19 +38,8 @@ stroke-width (case (:stroke-alignment shape :center) :center (/ (:stroke-width shape 0) 2) :outer (:stroke-width shape 0) - 0) - margin (gsh/shape-stroke-margin shape stroke-width) - bounding-box (-> (gsh/points->selrect (:points shape)) - (update :x - (+ stroke-width margin)) - (update :y - (+ stroke-width margin)) - (update :width + (* 2 (+ stroke-width margin))) - (update :height + (* 2 (+ stroke-width margin))))] - [:mask {:id stroke-mask-id - :x (:x bounding-box) - :y (:y bounding-box) - :width (:width bounding-box) - :height (:height bounding-box) - :maskUnits "userSpaceOnUse"} + 0)] + [:mask {:id stroke-mask-id} [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 0d5e98d8d..d223087d2 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -19,11 +19,12 @@ (let [valign (:vertical-align node "top") base #js {:height "100%" :width "100%" - :fontFamily "sourcesanspro"}] + :fontFamily "sourcesanspro" + :display "flex"}] (cond-> base - (= valign "top") (obj/set! "justifyContent" "flex-start") - (= valign "center") (obj/set! "justifyContent" "center") - (= valign "bottom") (obj/set! "justifyContent" "flex-end")))) + (= valign "top") (obj/set! "alignItems" "flex-start") + (= valign "center") (obj/set! "alignItems" "center") + (= valign "bottom") (obj/set! "alignItems" "flex-end")))) (defn generate-paragraph-set-styles [{:keys [grow-type] :as shape}] @@ -39,7 +40,6 @@ #js {:display "inline-flex" :flexDirection "column" :justifyContent "inherit" - :minHeight (when-not (or auto-width? auto-height?) "100%") :minWidth (when-not auto-width? "100%") :marginRight "1px" :verticalAlign "top"})) 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 145723ec2..468ffbe08 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -24,9 +24,8 @@ (let [render-id (mf/use-ctx muc/render-ctx) {:keys [id x y width height position-data] :as shape} (obj/get props "shape") - clip-id (str "clip-text" id "_" render-id) - group-props (-> #js {:transform (gsh/transform-matrix shape) - :clipPath (str "url(#" clip-id ")")} + transform (str (gsh/transform-matrix shape)) + group-props (-> #js {:transform transform} (attrs/add-style-attrs shape render-id)) get-gradient-id (fn [index] @@ -43,9 +42,6 @@ [:& shape-custom-stroke {:shape shape} [:> :g group-props - [:defs - [:clipPath {:id clip-id} - [: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) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index b2589c91e..e6ee44b87 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -131,7 +131,6 @@ handle-change-foreign-object (fn [node] (when (some? node) - (prn "change!") (let [position-data (utp/calc-position-data node) parent (dom/get-parent node) parent-transform (dom/get-attribute parent "transform") @@ -174,7 +173,7 @@ ;; and updates the selrect accordingly [:* [:g.text-shape {:ref on-change-node - :opacity (when (or edition? (some? (:position-data shape))) 0.2) + :opacity (when (or edition? (some? (:position-data shape))) 0) :pointer-events "none"} ;; The `:key` prop here is mandatory because the 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 4278a005c..e665329c1 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.text :as txt] [app.main.data.workspace :as dw] @@ -211,7 +212,7 @@ [:div.text-editor {:ref self-ref - :style {:cursor cur/text + :style {:cursor (cur/text (:rotation shape)) :width (:width shape) :height (:height shape)} :on-click on-click @@ -231,61 +232,34 @@ :ref on-editor :editor-state state}]])) -(mf/defc text-shape-edit - {::mf/wrap [mf/memo] - ::mf/wrap-props false - ::mf/forward-ref true} - [props _] - (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape") - transform (str (gsh/transform-matrix shape)) +(defn translate-point-from-viewport + "Translate a point in the viewport into client coordinates" + [pt viewport zoom] + (let [vbox (.. ^js viewport -viewBox -baseVal) + box (gpt/point (.-x vbox) (.-y vbox)) + zoom (gpt/point zoom)] + (-> (gpt/subtract pt box) + (gpt/multiply zoom)))) - clip-id (str "clip-" id) +(mf/defc text-editor-viewport + {::mf/wrap-props false} + [props] + (let [shape (obj/get props "shape") + viewport-ref (obj/get props "viewport-ref") + zoom (obj/get props "zoom") - ;; local-position-data (mf/use-state nil) + position + (-> (gpt/point (-> shape :selrect :x) + (-> shape :selrect :y)) + (translate-point-from-viewport (mf/ref-val viewport-ref) zoom))] - ;;handle-change-foreign-object - ;;(mf/use-callback - ;; (fn [node] - ;; (when node - ;; (let [position-data (utp/calc-position-data node)] - ;; (reset! local-position-data position-data))))) - ;; - ;;[shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object) + [:div {:style {:position "absolute" + :left (str (:x position) "px") + :top (str (:y position) "px") + :pointer-events "all" + :transform (str (gsh/transform-matrix shape nil (gpt/point 0 0))) + :transform-origin "center center"}} - ;;handle-interaction - ;;(mf/use-callback - ;; (fn [] - ;; (handle-change-foreign-object (mf/ref-val shape-ref)))) - ] - - #_(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)}]]]])) + [:div {:style {:transform (str "scale(" zoom ")") + :transform-origin "top left"}} + [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index fd6fc1afd..81286e698 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -110,6 +110,8 @@ ;; Only when we have all the selected shapes in one frame selected-frame (when (= (count selected-frames) 1) (get base-objects (first selected-frames))) + editing-shape (when edition (get base-objects edition)) + create-comment? (= :comments drawing-tool) drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) @@ -159,6 +161,8 @@ show-artboard-names? (contains? layout :display-artboard-names) show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) + show-text-editor? (and editing-shape (= :text (:type editing-shape))) + disabled-guides? (or drawing-tool transform)] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) @@ -176,6 +180,10 @@ [:& wtr/frame-renderer {:objects base-objects :background background}] + (when show-text-editor? + [:& editor/text-editor-viewport {:shape editing-shape + :viewport-ref viewport-ref + :zoom zoom}]) (when show-comments? [:& comments/comments-layer {:vbox vbox :vport vport @@ -268,9 +276,6 @@ :hover-shape @hover :zoom zoom}]) - (when text-editing? - [:& editor/text-shape-edit {:shape (get base-objects edition)}]) - [:& widgets/frame-titles {:objects objects-modified :selected selected From bbf91a8957d04490a1f77edb263a0db7d79c3684 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Feb 2022 16:49:14 +0100 Subject: [PATCH 07/16] :sparkles: Improved text selection --- common/src/app/common/geom/shapes/common.cljc | 12 +++--- .../src/app/common/geom/shapes/intersect.cljc | 42 ++++++++++++++++--- common/src/app/common/geom/shapes/text.cljc | 27 ++++++++++++ frontend/src/app/main/ui/shapes/attrs.cljs | 4 +- .../app/main/ui/workspace/viewport/hooks.cljs | 13 ------ frontend/src/app/worker/selection.cljs | 11 ++++- 6 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 common/src/app/common/geom/shapes/text.cljc diff --git a/common/src/app/common/geom/shapes/common.cljc b/common/src/app/common/geom/shapes/common.cljc index aa0a655ec..0cd9d4704 100644 --- a/common/src/app/common/geom/shapes/common.cljc +++ b/common/src/app/common/geom/shapes/common.cljc @@ -70,9 +70,11 @@ ([points matrix] (transform-points points nil matrix)) ([points center matrix] - (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) - post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) + (if (some? matrix) + (let [prev (if center (gmt/translate-matrix center) (gmt/matrix)) + post (if center (gmt/translate-matrix (gpt/negate center)) (gmt/matrix)) - tr-point (fn [point] - (gpt/transform point (gmt/multiply prev matrix post)))] - (mapv tr-point points)))) + tr-point (fn [point] + (gpt/transform point (gmt/multiply prev matrix post)))] + (mapv tr-point points)) + points))) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index 7b8925dc0..a61bd96f7 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -9,8 +9,10 @@ [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.path :as gpp] [app.common.geom.shapes.rect :as gpr] + [app.common.geom.shapes.text :as gte] [app.common.math :as mth])) (defn orientation @@ -283,6 +285,23 @@ (is-point-inside-ellipse? (first rect-points) ellipse-data) (intersects-lines-ellipse? rect-lines ellipse-data)))) +(defn overlaps-text? + [{:keys [position-data] :as shape} rect] + + (if (and (some? position-data) (d/not-empty? position-data)) + (let [center (gco/center-shape shape) + + transform-rect + (fn [rect-points] + (gco/transform-points rect-points center (:transform shape)))] + + (->> position-data + (map (comp transform-rect + gpr/rect->points + gte/position-data->rect)) + (some #(overlaps-rect-points? rect %)))) + (overlaps-rect-points? rect (:points shape)))) + (defn overlaps? "General case to check for overlapping between shapes and a rectangle" [shape rect] @@ -291,14 +310,25 @@ (update :x - stroke-width) (update :y - stroke-width) (update :width + (* 2 stroke-width)) - (update :height + (* 2 stroke-width)) - )] + (update :height + (* 2 stroke-width)))] (or (not shape) (let [path? (= :path (:type shape)) - circle? (= :circle (:type shape))] - (and (overlaps-rect-points? rect (:points shape)) - (or (not path?) (overlaps-path? shape rect)) - (or (not circle?) (overlaps-ellipse? shape rect))))))) + circle? (= :circle (:type shape)) + text? (= :text (:type shape))] + (cond + path? + (and (overlaps-rect-points? rect (:points shape)) + (overlaps-path? shape rect)) + + circle? + (and (overlaps-rect-points? rect (:points shape)) + (overlaps-ellipse? shape rect)) + + text? + (overlaps-text? shape rect) + + :else + (overlaps-rect-points? rect (:points shape))))))) (defn has-point-rect? [rect point] diff --git a/common/src/app/common/geom/shapes/text.cljc b/common/src/app/common/geom/shapes/text.cljc new file mode 100644 index 000000000..6058324ea --- /dev/null +++ b/common/src/app/common/geom/shapes/text.cljc @@ -0,0 +1,27 @@ +;; 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.common.geom.shapes.text + (:require + [app.common.geom.shapes.common :as gco] + [app.common.geom.shapes.rect :as gpr] + [app.common.geom.shapes.transforms :as gtr])) + +(defn position-data->rect + [{:keys [x y width height]}] + {:x x + :y (- y height) + :width width + :height height}) + +(defn position-data-bounding-box + [{:keys [position-data] :as shape}] + (let [points (->> position-data + (mapcat (comp gpr/rect->points position-data->rect))) + transform (gtr/transform-matrix shape) + points (gco/transform-points points transform)] + (gpr/points->selrect points))) + diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 3a30cf7f3..c969e2f4e 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -82,7 +82,7 @@ (defn add-fill ([attrs shape render-id] - (add-fill attrs shape render-id 0)) + (add-fill attrs shape render-id nil)) ([attrs shape render-id index] (let [fill-attrs @@ -92,7 +92,7 @@ {:fill (str/format "url(#%s)" fill-image-id)}) (contains? shape :fill-color-gradient) - (let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)] + (let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))] {:fill (str/format "url(#%s)" fill-color-gradient-id)}) (contains? shape :fill-color) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 81a9352ba..8af6c2aad 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -98,16 +98,6 @@ (some #(cph/is-parent? objects % group-id)) (not)))) -(defn check-text-collision? - "Checks if he current position `pos` overlaps any of the text-nodes for the given `text-id`" - [objects pos text-id] - (and (= :text (get-in objects [text-id :type])) - (let [collisions - (->> (dom/query-all (str "#shape-" text-id " .text-node")) - (map dom/get-bounding-rect) - (map dom/bounding-rect->rect))] - (not (some #(gshr/contains-point? % pos) collisions))))) - (defn setup-hover-shapes [page-id move-stream raw-position-ref objects transform selected ctrl? hover hover-ids hover-disabled? zoom] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) @@ -180,9 +170,6 @@ remove-xfm (mapcat #(cph/get-parent-ids objects %)) remove-id? (cond-> (into #{} remove-xfm selected) - :always - (into (filter #(check-text-collision? objects (mf/ref-val raw-position-ref) %)) ids) - (not @ctrl?) (into (filter #(group-empty-space? % objects ids)) ids) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 30879d60b..74abc06dc 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -8,6 +8,7 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.text :as gte] [app.common.pages :as cp] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] @@ -23,7 +24,15 @@ (defn index-shape [objects parents-index clip-parents-index] (fn [index shape] - (let [{:keys [x y width height]} (gsh/points->selrect (:points shape)) + (let [{:keys [x y width height]} + (cond + (and (= :text (:type shape)) + (some? (:position-data shape)) + (d/not-empty? (:position-data shape))) + (gte/position-data-bounding-box shape) + + :else + (gsh/points->selrect (:points shape))) shape-bound #js {:x x :y y :width width :height height} parents (get parents-index (:id shape)) From e183d67e2a14420fb3ae10c70aceadd68c7d17c5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Feb 2022 18:10:28 +0100 Subject: [PATCH 08/16] :sparkles: Add spec for new text data --- backend/src/app/http/errors.clj | 10 ++-- common/src/app/common/spec/shape.cljc | 43 ++++++++++++++++- .../src/app/main/ui/shapes/text/styles.cljs | 3 +- .../src/app/main/ui/shapes/text/svg_text.cljs | 2 +- .../app/main/ui/workspace/shapes/text.cljs | 26 +++++----- .../main/ui/workspace/shapes/text/editor.cljs | 4 -- .../src/app/main/ui/workspace/viewport.cljs | 5 +- .../main/ui/workspace/viewport/actions.cljs | 3 +- .../app/main/ui/workspace/viewport/hooks.cljs | 3 +- frontend/src/app/util/text_svg_position.cljs | 48 +++++++++---------- 10 files changed, 93 insertions(+), 54 deletions(-) diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 0c24a3df7..c7f2a9ba6 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -61,13 +61,13 @@ explain (us/pretty-explain data)] {:status 400 :body (-> data - (dissoc ::s/problems) - (dissoc ::s/value) + (dissoc ::s/problems ::s/value) (cond-> explain (assoc :explain explain)))})) (defmethod handle-exception :assertion [error request] - (let [edata (ex-data error)] + (let [edata (ex-data error) + explain (us/pretty-explain edata)] (l/error ::l/raw (ex-message error) ::l/context (get-error-context request error) :cause error) @@ -75,7 +75,9 @@ {:status 500 :body {:type :server-error :code :assertion - :data (dissoc edata ::s/problems ::s/value ::s/spec)}})) + :data (-> edata + (dissoc ::s/problems ::s/value ::s/spec) + (cond-> explain (assoc :explain explain)))}})) (defmethod handle-exception :not-found [err _] diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc index 08ab1b023..54853d259 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/spec/shape.cljc @@ -208,6 +208,46 @@ :text-content (s/keys :req-un [:internal.shape.text/text])))) +(s/def :internal.shape.text/position-data + (s/coll-of :internal.shape.text/position-data-element + :kind vector? + :min-count 1)) + +(s/def :internal.shape.text/position-data-element + (s/keys :req-un [:internal.shape.text.position-data/x + :internal.shape.text.position-data/y + :internal.shape.text.position-data/width + :internal.shape.text.position-data/height] + :opt-un [:internal.shape.text.position-data/fill-color + :internal.shape.text.position-data/fill-opacity + :internal.shape.text.position-data/font-family + :internal.shape.text.position-data/font-size + :internal.shape.text.position-data/font-style + :internal.shape.text.position-data/font-weight + :internal.shape.text.position-data/rtl? + :internal.shape.text.position-data/text + :internal.shape.text.position-data/text-decoration + :internal.shape.text.position-data/text-transform] + )) + +(s/def :internal.shape.text.position-data/x ::us/safe-number) +(s/def :internal.shape.text.position-data/y ::us/safe-number) +(s/def :internal.shape.text.position-data/width ::us/safe-number) +(s/def :internal.shape.text.position-data/height ::us/safe-number) + +(s/def :internal.shape.text.position-data/fill-color ::fill-color) +(s/def :internal.shape.text.position-data/fill-opacity ::fill-opacity) +(s/def :internal.shape.text.position-data/fill-color-gradient ::fill-color-gradient) + +(s/def :internal.shape.text.position-data/font-family string?) +(s/def :internal.shape.text.position-data/font-size string?) +(s/def :internal.shape.text.position-data/font-style string?) +(s/def :internal.shape.text.position-data/font-weight string?) +(s/def :internal.shape.text.position-data/rtl? boolean?) +(s/def :internal.shape.text.position-data/text string?) +(s/def :internal.shape.text.position-data/text-decoration string?) +(s/def :internal.shape.text.position-data/text-transform string?) + (s/def :internal.shape.path/command keyword?) (s/def :internal.shape.path/params (s/nilable (s/map-of keyword? any?))) @@ -226,7 +266,8 @@ (defmethod shape-spec :text [_] (s/and ::shape-attrs - (s/keys :opt-un [:internal.shape.text/content]))) + (s/keys :opt-un [:internal.shape.text/content + :internal.shape.text/position-data]))) (defmethod shape-spec :path [_] (s/and ::shape-attrs diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index d223087d2..fc65f836b 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -35,8 +35,7 @@ ;; the property it's known. ;; `inline-flex` is similar to flex but `overflows` outside the bounds of the ;; parent - (let [auto-width? (= grow-type :auto-width) - auto-height? (= grow-type :auto-height)] + (let [auto-width? (= grow-type :auto-width)] #js {:display "inline-flex" :flexDirection "column" :justifyContent "inherit" 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 468ffbe08..2be61f6da 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -23,7 +23,7 @@ [props] (let [render-id (mf/use-ctx muc/render-ctx) - {:keys [id x y width height position-data] :as shape} (obj/get props "shape") + {:keys [position-data] :as shape} (obj/get props "shape") transform (str (gsh/transform-matrix shape)) group-props (-> #js {:transform transform} (attrs/add-style-attrs shape render-id)) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index e6ee44b87..ca7a373c7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -7,6 +7,7 @@ (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] @@ -150,7 +151,9 @@ (gsh/transform-rect mtx)))))] (reset! local-position-data position-data)))) - [shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object)] + [shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object) + + show-svg-text? (or (some? (:position-data shape)) (some? @local-position-data))] ;; When the text is "dirty?" we get recalculate the positions (mf/use-layout-effect @@ -159,21 +162,22 @@ (let [node (mf/ref-val shape-ref)] (when (and dirty? (some? node)) (let [position-data (utp/calc-position-data node)] - (reset! local-position-data nil) - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (dissoc :dirty?) - (assoc :position-data position-data))) - {:save-undo? false}))))))) + (when (d/not-empty? position-data) + (reset! local-position-data nil) + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (dissoc :dirty?) + (assoc :position-data position-data))) + {:save-undo? false})))))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly [:* [:g.text-shape {:ref on-change-node - :opacity (when (or edition? (some? (:position-data shape))) 0) + :opacity (when (or edition? show-svg-text?) 0) :pointer-events "none"} ;; The `:key` prop here is mandatory because the @@ -186,7 +190,7 @@ :edition? edition? :key (str id edition?)}]] - (when (and (or (some? (:position-data shape)) (some? local-position-data))) + (when show-svg-text? (let [shape (cond-> shape (some? @local-position-data) 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 e665329c1..ad6ea6f9b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -15,15 +15,11 @@ [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 diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 81286e698..0430a8dd6 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -84,7 +84,6 @@ ;; REFS viewport-ref (mf/use-ref nil) - raw-position-ref (mf/use-ref nil) ;; Stores the raw position of the cursor ;; VARS disable-paste (mf/use-var false) @@ -130,7 +129,7 @@ on-pointer-down (actions/on-pointer-down) on-pointer-enter (actions/on-pointer-enter in-viewport?) on-pointer-leave (actions/on-pointer-leave in-viewport?) - on-pointer-move (actions/on-pointer-move viewport-ref raw-position-ref zoom move-stream) + on-pointer-move (actions/on-pointer-move viewport-ref zoom move-stream) on-pointer-up (actions/on-pointer-up) on-move-selected (actions/on-move-selected hover hover-ids selected space?) on-menu-selected (actions/on-menu-selected hover hover-ids selected) @@ -169,7 +168,7 @@ (hooks/setup-viewport-size viewport-ref) (hooks/setup-cursor cursor alt? ctrl? space? panning drawing-tool drawing-path? node-editing?) (hooks/setup-keyboard alt? ctrl? space?) - (hooks/setup-hover-shapes page-id move-stream raw-position-ref base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) + (hooks/setup-hover-shapes page-id move-stream base-objects transform selected ctrl? hover hover-ids @hover-disabled? zoom) (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames base-objects vbox hover active-frames) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 0d6ca0ba8..50bfd6b12 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -350,14 +350,13 @@ (kbd/shift? event) (kbd/alt? event)))))))) -(defn on-pointer-move [viewport-ref raw-position-ref zoom move-stream] +(defn on-pointer-move [viewport-ref zoom move-stream] (mf/use-callback (mf/deps zoom move-stream) (fn [event] (let [raw-pt (dom/get-client-position event) viewport (mf/ref-val viewport-ref) pt (utils/translate-point-to-viewport viewport zoom raw-pt)] - (mf/set-ref-val! raw-position-ref raw-pt) (rx/push! move-stream pt))))) (defn on-mouse-wheel [viewport-ref zoom] diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 8af6c2aad..0cb64935b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -8,7 +8,6 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.common.geom.shapes.rect :as gshr] [app.common.pages.helpers :as cph] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] @@ -98,7 +97,7 @@ (some #(cph/is-parent? objects % group-id)) (not)))) -(defn setup-hover-shapes [page-id move-stream raw-position-ref objects transform selected ctrl? hover hover-ids hover-disabled? zoom] +(defn setup-hover-shapes [page-id move-stream objects transform selected ctrl? hover hover-ids hover-disabled? zoom] (let [;; We use ref so we don't recreate the stream on a change zoom-ref (mf/use-ref zoom) ctrl-ref (mf/use-ref @ctrl?) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index 6f30c6864..624aa4327 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -99,27 +99,27 @@ 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}))))))) + (mapv (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}))))))) From 96eacb6efefa06d49e993f94a01d40e516d05b93 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 18 Feb 2022 11:15:21 +0100 Subject: [PATCH 09/16] :sparkles: Changed update text flow --- .../app/common/geom/shapes/transforms.cljc | 9 ++- .../resources/styles/main/partials/texts.scss | 1 - .../src/app/main/data/workspace/texts.cljs | 5 +- .../app/main/data/workspace/transforms.cljs | 2 +- .../src/app/main/ui/shapes/text/styles.cljs | 3 +- .../app/main/ui/workspace/shapes/text.cljs | 40 ++++++------ frontend/src/app/util/text_svg_position.cljs | 62 +++++++++---------- 7 files changed, 61 insertions(+), 61 deletions(-) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index a63623322..a2d331232 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -43,9 +43,9 @@ [position-data dx dy] (->> position-data - (map #(-> % - (update :x + dx) - (update :y + dy))))) + (mapv #(-> % + (update :x + dx) + (update :y + dy))))) (defn move "Move the shape relatively to its current @@ -542,8 +542,7 @@ :else (let [shape (apply-displacement shape) - modifiers (:modifiers shape) - shape (cond-> shape (= :text (:type shape)) (assoc :dirty? true))] + modifiers (:modifiers shape)] (cond-> shape (not (empty-modifiers? modifiers)) (-> (set-flip modifiers) diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index d84442eb1..41b258e15 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -3,7 +3,6 @@ .rich-text { color: $color-black; height: 100%; - white-space: pre-wrap; font-family: sourcesanspro; div { diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 0c0f79228..c643335ba 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -65,7 +65,7 @@ (when (and (not= content (:content shape)) (some? (:current-page-id state))) (rx/of - (dch/update-shapes [id] #(assoc % :content content :dirty? true)) + (dch/update-shapes [id] #(assoc % :content content)) (dwu/commit-undo-transaction))))) (when (some? id) @@ -150,8 +150,7 @@ (let [merge-attrs #(merge-fn % attrs) transform #(txt/transform-nodes pred-fn merge-attrs %)] (-> shape - (update :content transform) - (assoc :dirty? true)))) + (update :content transform)))) (defn update-root-attrs [{:keys [id attrs]}] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 699bd919d..d81e2bc77 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -185,7 +185,7 @@ :transform :transform-inverse :rotation - :dirty? + :position-data :flip-x :flip-y]}) (clear-local-transform) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index fc65f836b..3e21da621 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -81,7 +81,8 @@ :textTransform text-transform :lineHeight (or line-height "inherit") :color "transparent" - :caretColor (or text-color "black")} + :caretColor (or text-color "black") + :overflowWrap "initial"} base (-> base (obj/set! "--fill-color" fill-color) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index ca7a373c7..b62cdcfb8 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -123,7 +123,7 @@ (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [{:keys [id dirty?] :as shape} (unchecked-get props "shape") + (let [{:keys [id content points] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref) @@ -131,9 +131,8 @@ handle-change-foreign-object (fn [node] - (when (some? node) - (let [position-data (utp/calc-position-data node) - parent (dom/get-parent node) + (when-let [position-data (utp/calc-position-data node)] + (let [parent (dom/get-parent node) parent-transform (dom/get-attribute parent "transform") node-transform (dom/get-attribute node "transform") @@ -153,24 +152,27 @@ [shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object) - show-svg-text? (or (some? (:position-data shape)) (some? @local-position-data))] + show-svg-text? (or (some? (:position-data shape)) (some? @local-position-data)) + + update-position-data + (fn [] + (when (some? @local-position-data) + (reset! local-position-data nil) + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (assoc :position-data @local-position-data))) + {:save-undo? false}))))] - ;; When the text is "dirty?" we get recalculate the positions (mf/use-layout-effect - (mf/deps id dirty?) + (mf/deps @local-position-data) (fn [] - (let [node (mf/ref-val shape-ref)] - (when (and dirty? (some? node)) - (let [position-data (utp/calc-position-data node)] - (when (d/not-empty? position-data) - (reset! local-position-data nil) - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (dissoc :dirty?) - (assoc :position-data position-data))) - {:save-undo? false})))))))) + ;; Timer to update the shape. We do this so a lot of changes won't produce + ;; a lot of updates (kind of a debounce) + (let [sid (timers/schedule 250 update-position-data)] + (fn [] + (rx/dispose! sid))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index 624aa4327..ee9a0f0ba 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -54,7 +54,7 @@ (defn calc-text-node-positions [base-node viewport zoom] - (when (some? viewport) + (when (and (some? base-node)(some? viewport)) (let [translate-point (fn [pt] (let [vbox (.. ^js viewport -viewBox -baseVal) @@ -89,37 +89,37 @@ (let [rtl? (= "rtl" (.-dir (.-parentElement parent-node)))] (->> (.-childNodes parent-node) (mapcat #(parse-text-nodes parent-node rtl? %)))))) - (map #(update % :position translate-rect)))))) - - + (mapv #(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 - (mapv (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}))))))) + zoom (or (get-in @st/state [:workspace-local :zoom]) 1)] + (when (and (some? base-node) (some? viewport)) + (let [text-data (calc-text-node-positions base-node viewport zoom)] + (when (d/not-empty? text-data) + (->> text-data + (mapv (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})))))))))) From d0e008665fa9b6fb49ad9362b2e9fc79cc21a912 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 18 Feb 2022 13:50:54 +0100 Subject: [PATCH 10/16] :sparkles: Fix masks for Firefox --- common/src/app/common/geom/shapes/text.cljc | 12 ++- frontend/src/app/main/ui/shapes/mask.cljs | 74 +++++++++++++------ .../app/main/ui/workspace/shapes/text.cljs | 7 +- 3 files changed, 64 insertions(+), 29 deletions(-) diff --git a/common/src/app/common/geom/shapes/text.cljc b/common/src/app/common/geom/shapes/text.cljc index 6058324ea..a23cca61a 100644 --- a/common/src/app/common/geom/shapes/text.cljc +++ b/common/src/app/common/geom/shapes/text.cljc @@ -17,11 +17,15 @@ :width width :height height}) -(defn position-data-bounding-box +(defn position-data-points [{:keys [position-data] :as shape}] (let [points (->> position-data (mapcat (comp gpr/rect->points position-data->rect))) - transform (gtr/transform-matrix shape) - points (gco/transform-points points transform)] - (gpr/points->selrect points))) + transform (gtr/transform-matrix shape)] + (gco/transform-points points transform))) + +(defn position-data-bounding-box + [shape] + (gpr/points->selrect (position-data-points shape))) + diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index 30a5ed70f..a9b58db4c 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -6,7 +6,9 @@ (ns app.main.ui.shapes.mask (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.text :as gst] [app.main.ui.context :as muc] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -29,6 +31,17 @@ (defn filter-url [render-id mask] (str "url(#" (filter-id render-id mask) ")")) +(defn set-white-fill + [shape] + (let [update-color + (fn [data] + (-> data + (dissoc :fill-color :fill-opacity :fill-color-gradient) + (assoc :fill-color "#FFFFFF" :fill-opacity 1)))] + (-> shape + (d/update-when :position-data #(mapv update-color %)) + (assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) + (defn mask-factory [shape-wrapper] (mf/fnc mask-shape @@ -36,25 +49,44 @@ [props] (let [mask (unchecked-get props "mask") render-id (mf/use-ctx muc/render-ctx) - mask' (gsh/transform-shape mask)] - [:defs - [:filter {:id (filter-id render-id mask)} - [:feFlood {:flood-color "white" - :result "FloodResult"}] - [:feComposite {:in "FloodResult" - :in2 "SourceGraphic" - :operator "in" - :result "comp"}]] - ;; Clip path is necessary so the elements inside the mask won't affect - ;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility) - ;; we cannot use clips instead of mask because clips can only be simple shapes - [:clipPath {:class "mask-clip-path" - :id (clip-id render-id mask)} - [:polyline {:points (->> (:points mask') - (map #(str (:x %) "," (:y %))) - (str/join " "))}]] - [:mask {:class "mask-shape" - :id (mask-id render-id mask)} - [:g {:filter (filter-url render-id mask)} - [:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]]]))) + svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) + + mask (cond-> mask svg-text? set-white-fill) + + mask-bb + (cond + svg-text? + (gst/position-data-points mask) + + :else + (-> (gsh/transform-shape mask) + (:points)))] + [:* + [:g {:opacity 0} + [:g {:id (str "shape-" (mask-id render-id mask))} + [:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]] + + [:defs + [:filter {:id (filter-id render-id mask)} + [:feFlood {:flood-color "white" + :result "FloodResult"}] + [:feComposite {:in "FloodResult" + :in2 "SourceGraphic" + :operator "in" + :result "comp"}]] + ;; Clip path is necessary so the elements inside the mask won't affect + ;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility) + ;; we cannot use clips instead of mask because clips can only be simple shapes + [:clipPath {:class "mask-clip-path" + :id (clip-id render-id mask)} + [:polyline {:points (->> mask-bb + (map #(str (:x %) "," (:y %))) + (str/join " "))}]] + + [:mask {:class "mask-shape" + :id (mask-id render-id mask)} + ;; SVG texts are broken in Firefox with the filter. When the masking shapes is a text + ;; we use the `set-white-fill` instead of using the filter + [:g {:filter (when-not svg-text? (filter-url render-id mask))} + [:use {:href (str "#shape-" (mask-id render-id mask))}]]]]]))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index b62cdcfb8..36e82d864 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -7,7 +7,6 @@ (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] @@ -123,7 +122,7 @@ (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [{:keys [id content points] :as shape} (unchecked-get props "shape") + (let [{:keys [id] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref) @@ -150,7 +149,7 @@ (gsh/transform-rect mtx)))))] (reset! local-position-data position-data)))) - [shape-ref on-change-node] (use-mutable-observer handle-change-foreign-object) + [_ on-change-node] (use-mutable-observer handle-change-foreign-object) show-svg-text? (or (some? (:position-data shape)) (some? @local-position-data)) @@ -170,7 +169,7 @@ (fn [] ;; Timer to update the shape. We do this so a lot of changes won't produce ;; a lot of updates (kind of a debounce) - (let [sid (timers/schedule 250 update-position-data)] + (let [sid (timers/schedule 100 update-position-data)] (fn [] (rx/dispose! sid))))) From 3228582cbec187c97a01c66210a916a79c623e91 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Feb 2022 11:15:29 +0100 Subject: [PATCH 11/16] :sparkles: Fix problems when migrating old texts --- .../src/app/main/ui/shapes/text/styles.cljs | 97 ++++++++++--------- .../app/main/ui/workspace/shapes/text.cljs | 36 ++++++- .../main/ui/workspace/shapes/text/editor.cljs | 4 +- 3 files changed, 83 insertions(+), 54 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 3e21da621..e23225338 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -58,60 +58,63 @@ (= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))) (defn generate-text-styles - [data] - (let [letter-spacing (:letter-spacing data 0) - text-decoration (:text-decoration data) - text-transform (:text-transform data) - line-height (:line-height data 1.2) + ([data] + (generate-text-styles data nil)) - font-id (:font-id data (:font-id txt/default-text-attrs)) - font-variant-id (:font-variant-id data) + ([data {:keys [show-text?] :or {show-text? true}}] + (let [letter-spacing (:letter-spacing data 0) + text-decoration (:text-decoration data) + text-transform (:text-transform data) + line-height (:line-height data 1.2) - font-size (:font-size data) - fill-color (:fill-color data) - fill-opacity (:fill-opacity data) + font-id (:font-id data (:font-id txt/default-text-attrs)) + font-variant-id (:font-variant-id data) - [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)) + font-size (:font-size data) + fill-color (:fill-color data) + fill-opacity (:fill-opacity data) - fontsdb (deref fonts/fontsdb) + [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)) - base #js {:textDecoration text-decoration - :textTransform text-transform - :lineHeight (or line-height "inherit") - :color "transparent" - :caretColor (or text-color "black") - :overflowWrap "initial"} + fontsdb (deref fonts/fontsdb) - 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))] + base #js {:textDecoration text-decoration + :textTransform text-transform + :lineHeight (or line-height "inherit") + :color (if show-text? text-color "transparent") + :caretColor (or text-color "black") + :overflowWrap "initial"} - (when (and (string? letter-spacing) - (pos? (alength letter-spacing))) - (obj/set! base "letterSpacing" (str letter-spacing "px"))) + 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? font-size) - (pos? (alength font-size))) - (obj/set! base "fontSize" (str font-size "px"))) + (when (and (string? letter-spacing) + (pos? (alength letter-spacing))) + (obj/set! base "letterSpacing" (str letter-spacing "px"))) - (when (and (string? font-id) - (pos? (alength font-id))) - (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id) - font-family (str/quote - (or (:family font) - (:font-family data))) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (:font-style data)) - font-weight (or (:weight font-variant) - (:font-weight data))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight))) + (when (and (string? font-size) + (pos? (alength font-size))) + (obj/set! base "fontSize" (str font-size "px"))) - base)) + (when (and (string? font-id) + (pos? (alength font-id))) + (fonts/ensure-loaded! font-id) + (let [font (get fontsdb font-id) + font-family (str/quote + (or (:family font) + (:font-family data))) + font-variant (d/seek #(= font-variant-id (:id %)) + (:variants font)) + font-style (or (:style font-variant) + (:font-style data)) + font-weight (or (:weight font-variant) + (:font-weight data))] + (obj/set! base "fontFamily" font-family) + (obj/set! base "fontStyle" font-style) + (obj/set! base "fontWeight" font-weight))) + + base))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 36e82d864..dfdfc1093 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -122,12 +122,14 @@ (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [{:keys [id] :as shape} (unchecked-get props "shape") + (let [{:keys [id position-data] :as shape} (unchecked-get props "shape") edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) edition? (mf/deref edition-ref) local-position-data (mf/use-state nil) + sid-ref (mf/use-ref nil) + handle-change-foreign-object (fn [node] (when-let [position-data (utp/calc-position-data node)] @@ -149,9 +151,9 @@ (gsh/transform-rect mtx)))))] (reset! local-position-data position-data)))) - [_ on-change-node] (use-mutable-observer handle-change-foreign-object) + [node-ref on-change-node] (use-mutable-observer handle-change-foreign-object) - show-svg-text? (or (some? (:position-data shape)) (some? @local-position-data)) + show-svg-text? (or (some? position-data) (some? @local-position-data)) update-position-data (fn [] @@ -173,12 +175,36 @@ (fn [] (rx/dispose! sid))))) + (mf/use-layout-effect + (mf/deps show-svg-text?) + (fn [] + (let [] + (when-not show-svg-text? + ;; There is no position data we need to calculate it even if no change has happened + ;; this usualy happens the first time a text is rendered + (let [update-data + (fn update-data [] + (let [node (mf/ref-val node-ref)] + (if (some? node) + (let [position-data (utp/calc-position-data node)] + (reset! local-position-data position-data)) + + ;; No node present, we need to keep waiting + (do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid)) + (when-not @local-position-data + (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))] + (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))) + + (fn [] + (when-let [sid (mf/ref-val sid-ref)] + (rx/dispose! sid)))))) + [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size ;; and updates the selrect accordingly [:* [:g.text-shape {:ref on-change-node - :opacity (when (or edition? show-svg-text?) 0) + :opacity (when show-svg-text? 0) :pointer-events "none"} ;; The `:key` prop here is mandatory because the @@ -186,7 +212,7 @@ ;; the component if the edition flag changes. [:& text-resize-content {:shape (cond-> shape - (:position-data shape) + show-svg-text? (dissoc :transform :transform-inverse)) :edition? edition? :key (str id edition?)}]] 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 ad6ea6f9b..5d4b45552 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -62,9 +62,9 @@ (-> (.getData content) (.toJS) (js->clj :keywordize-keys true) - (sts/generate-text-styles)) + (sts/generate-text-styles {:show-text? false})) (-> (txt/styles-to-attrs styles) - (sts/generate-text-styles)))) + (sts/generate-text-styles {:show-text? false})))) (def default-decorator (ted/create-decorator "PENPOT_SELECTION" selection-component)) From 82c6b8daae907889e8e75faf2622b2bfe17cd8db Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Feb 2022 11:55:53 +0100 Subject: [PATCH 12/16] :sparkles: Fix problems with export/import --- frontend/src/app/main/ui/shapes/export.cljs | 30 ++++++------- .../src/app/main/ui/shapes/text/svg_text.cljs | 42 +++++++++++-------- .../app/main/ui/workspace/shapes/text.cljs | 35 ++++++++-------- frontend/src/app/util/import/parser.cljs | 14 +++++-- 4 files changed, 68 insertions(+), 53 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index b062f6ddd..df9770836 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -109,7 +109,8 @@ (cond-> text? (-> (add! :grow-type) - (add! :content (comp json/encode uuid->string)))) + (add! :content (comp json/encode uuid->string)) + (add! :position-data (comp json/encode uuid->string)))) (cond-> mask? (obj/set! "penpot:masked-group" "true")) @@ -138,19 +139,20 @@ (into {} (map prefix-entry) m))) (defn- export-grid-data [{:keys [grids]}] - (mf/html - [:> "penpot:grids" #js {} - (for [{:keys [type display params]} grids] - (let [props (->> (dissoc params :color) - (prefix-keys) - (clj->js))] - [:> "penpot:grid" - (-> props - (obj/set! "penpot:color" (get-in params [:color :color])) - (obj/set! "penpot:opacity" (get-in params [:color :opacity])) - (obj/set! "penpot:type" (d/name type)) - (cond-> (some? display) - (obj/set! "penpot:display" (str display))))]))])) + (when (d/not-empty? grids) + (mf/html + [:> "penpot:grids" #js {} + (for [{:keys [type display params]} grids] + (let [props (->> (dissoc params :color) + (prefix-keys) + (clj->js))] + [:> "penpot:grid" + (-> props + (obj/set! "penpot:color" (get-in params [:color :color])) + (obj/set! "penpot:opacity" (get-in params [:color :opacity])) + (obj/set! "penpot:type" (d/name type)) + (cond-> (some? display) + (obj/set! "penpot:display" (str display))))]))]))) (mf/defc export-flows [{:keys [flows]}] 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 2be61f6da..cd729f181 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -23,9 +23,15 @@ [props] (let [render-id (mf/use-ctx muc/render-ctx) - {:keys [position-data] :as shape} (obj/get props "shape") + {:keys [x y width height position-data] :as shape} (obj/get props "shape") transform (str (gsh/transform-matrix shape)) - group-props (-> #js {:transform transform} + + ;; These position attributes are not really necesary but they are convenient for for the export + group-props (-> #js {:transform transform + :x x + :y y + :width width + :height height} (attrs/add-style-attrs shape render-id)) get-gradient-id (fn [index] @@ -40,21 +46,21 @@ :attr :fill-color-gradient :shape data}]))]) - [:& shape-custom-stroke {:shape shape} - [:> :g group-props - (for [[index data] (d/enumerate position-data)] - (let [props (-> #js {:x (:x data) - :y (:y data) - :dominantBaseline "ideographic" - :style (-> #js {:fontFamily (:font-family data) - :fontSize (:font-size data) - :fontWeight (:font-weight data) - :textTransform (:text-transform data) - :textDecoration (:text-decoration data) - :fontStyle (:font-style data) - :direction (if (:rtl? data) "rtl" "ltr") - :whiteSpace "pre"} - (attrs/add-fill data (get-gradient-id index)))})] - [:> :text props (:text data)]))]]])) + [:> :g group-props + (for [[index data] (d/enumerate position-data)] + (let [props (-> #js {:x (:x data) + :y (:y data) + :dominantBaseline "ideographic" + :style (-> #js {:fontFamily (:font-family data) + :fontSize (:font-size data) + :fontWeight (:font-weight data) + :textTransform (:text-transform data) + :textDecoration (:text-decoration data) + :fontStyle (:font-style data) + :direction (if (:rtl? data) "rtl" "ltr") + :whiteSpace "pre"} + (attrs/add-fill data (get-gradient-id index)))})] + [:& shape-custom-stroke {:shape shape} + [:> :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 dfdfc1093..6e035fb94 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -178,26 +178,25 @@ (mf/use-layout-effect (mf/deps show-svg-text?) (fn [] - (let [] - (when-not show-svg-text? - ;; There is no position data we need to calculate it even if no change has happened - ;; this usualy happens the first time a text is rendered - (let [update-data - (fn update-data [] - (let [node (mf/ref-val node-ref)] - (if (some? node) - (let [position-data (utp/calc-position-data node)] - (reset! local-position-data position-data)) + (when-not show-svg-text? + ;; There is no position data we need to calculate it even if no change has happened + ;; this usualy happens the first time a text is rendered + (let [update-data + (fn update-data [] + (let [node (mf/ref-val node-ref)] + (if (some? node) + (let [position-data (utp/calc-position-data node)] + (reset! local-position-data position-data)) - ;; No node present, we need to keep waiting - (do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid)) - (when-not @local-position-data - (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))] - (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))) + ;; No node present, we need to keep waiting + (do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid)) + (when-not @local-position-data + (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))] + (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))) - (fn [] - (when-let [sid (mf/ref-val sid-ref)] - (rx/dispose! sid)))))) + (fn [] + (when-let [sid (mf/ref-val sid-ref)] + (rx/dispose! sid))))) [:> shape-container {:shape shape} ;; We keep hidden the shape when we're editing so it keeps track of the size diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index dac15ef0e..70edb5ec4 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -185,7 +185,7 @@ (d/deep-mapm (fn [pair] (->> pair (mapv convert))))))) -(def search-data-node? #{:rect :image :path :text :circle}) +(def search-data-node? #{:rect :image :path :circle}) (defn get-svg-data [type node] @@ -200,6 +200,13 @@ (map #(:attrs %)) (reduce add-attrs node-attrs))) + (= type :text) + (->> node + (node-seq) + (filter #(contains? #{:g :foreignObject} (:tag %))) + (map #(:attrs %)) + (reduce add-attrs node-attrs)) + (= type :frame) (let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))] (merge (add-attrs {} (:attrs svg-node)) node-attrs)) @@ -481,8 +488,9 @@ (defn add-text-data [props node] (-> props - (assoc :grow-type (get-meta node :grow-type keyword)) - (assoc :content (get-meta node :content (comp string->uuid json/decode))))) + (assoc :grow-type (get-meta node :grow-type keyword)) + (assoc :content (get-meta node :content (comp string->uuid json/decode))) + (assoc :position-data (get-meta node :position-data (comp string->uuid json/decode))))) (defn add-group-data [props node] From 40b7cafacce3afe8114c59f356c92feb05bd02b5 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Feb 2022 13:33:17 +0100 Subject: [PATCH 13/16] :sparkles: Fix problems with strokes --- .../src/app/main/ui/shapes/custom_stroke.cljs | 78 +++++++++++-------- frontend/src/app/main/ui/shapes/shape.cljs | 2 - .../src/app/main/ui/shapes/text/svg_text.cljs | 2 +- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 04e688a6f..3300bda30 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.shapes.custom-stroke (:require - [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] [app.util.object :as obj] @@ -25,16 +24,18 @@ (-> props (obj/merge #js {:style style})))) (mf/defc inner-stroke-clip-path - [{:keys [render-id]}] - (let [clip-id (str "inner-stroke-" render-id) - shape-id (str "stroke-shape-" render-id)] + [{:keys [shape render-id index]}] + (let [suffix (if index (str "-" index) "") + clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) + shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)] [:> "clipPath" #js {:id clip-id} [:use {:xlinkHref (str "#" shape-id)}]])) (mf/defc outer-stroke-mask - [{:keys [shape render-id]}] - (let [stroke-mask-id (str "outer-stroke-" render-id) - shape-id (str "stroke-shape-" render-id) + [{:keys [shape render-id index]}] + (let [suffix (if index (str "-" index) "") + stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) + shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix) stroke-width (case (:stroke-alignment shape :center) :center (/ (:stroke-width shape 0) 2) :outer (:stroke-width shape 0) @@ -44,7 +45,8 @@ :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] [:use {:xlinkHref (str "#" shape-id) - :style #js {:fill "black"}}]])) + :style #js {:fill "black" + :stroke "none"}}]])) (mf/defc cap-markers [{:keys [shape render-id]}] @@ -149,7 +151,7 @@ [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])])) (mf/defc stroke-defs - [{:keys [shape render-id]}] + [{:keys [shape render-id index]}] (let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))] (cond @@ -157,18 +159,21 @@ (= :inner (:stroke-alignment shape :center)) (> (:stroke-width shape 0) 0)) [:& inner-stroke-clip-path {:shape shape - :render-id render-id}] + :render-id render-id + :index index}] (and (not open-path?) (= :outer (:stroke-alignment shape :center)) (> (:stroke-width shape 0) 0)) [:& outer-stroke-mask {:shape shape - :render-id render-id}] + :render-id render-id + :index index}] (or (some? (:stroke-cap-start shape)) (some? (:stroke-cap-end shape))) [:& cap-markers {:shape shape - :render-id render-id}]))) + :render-id render-id + :index index}]))) ;; Outer alignment: display the shape in two layers. One ;; without stroke (only fill), and another one only with stroke @@ -179,40 +184,41 @@ {::mf/wrap-props false} [props] - (let [render-id (mf/use-ctx muc/render-ctx) - child (obj/get props "children") - base-props (obj/get child "props") - elem-name (obj/get child "type") - stroke-mask-id (str "outer-stroke-" render-id) - shape-id (str "stroke-shape-" render-id) + (let [render-id (mf/use-ctx muc/render-ctx) + child (obj/get props "children") + base-props (obj/get child "props") + elem-name (obj/get child "type") + shape (obj/get props "shape") + index (obj/get props "index") + stroke-width (:stroke-width shape) - style-str (->> (obj/get base-props "style") - (js->clj) - (mapv (fn [[k v]] - (-> (d/name k) - (str/kebab) - (str ":" v)))) - (str/join ";"))] + suffix (if index (str "-" index) "") + stroke-mask-id (str "outer-stroke-" render-id "-" (:id shape) suffix) + shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix)] [:g.outer-stroke-shape [:defs + [:& stroke-defs {:shape shape :render-id render-id :index index}] [:> elem-name (-> (obj/clone base-props) (obj/set! "id" shape-id) - (obj/set! "data-style" style-str) - (obj/without ["style"]))]] + (obj/set! + "style" + (-> (obj/get base-props "style") + (obj/clone) + (obj/without ["fill" "fillOpacity" "stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))))]] [:use {:xlinkHref (str "#" shape-id) :mask (str "url(#" stroke-mask-id ")") :style (-> (obj/get base-props "style") (obj/clone) - (obj/update! "strokeWidth" * 2) + (obj/set! "strokeWidth" (* stroke-width 2)) (obj/without ["fill" "fillOpacity"]) (obj/set! "fill" "none"))}] [:use {:xlinkHref (str "#" shape-id) :style (-> (obj/get base-props "style") (obj/clone) - (obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]])) + (obj/set! "stroke" "none"))}]])) ;; Inner alignment: display the shape with double width stroke, @@ -225,12 +231,14 @@ base-props (obj/get child "props") elem-name (obj/get child "type") shape (obj/get props "shape") + index (obj/get props "index") transform (obj/get base-props "transform") stroke-width (:stroke-width shape 0) - clip-id (str "inner-stroke-" render-id) - shape-id (str "stroke-shape-" render-id) + suffix (if index (str "-" index) "") + clip-id (str "inner-stroke-" render-id "-" (:id shape) suffix) + shape-id (str "stroke-shape-" render-id "-" (:id shape) suffix) clip-path (str "url('#" clip-id "')") shape-props (-> base-props @@ -240,12 +248,12 @@ [:g.inner-stroke-shape {:transform transform} [:defs + [:& stroke-defs {:shape shape :render-id render-id :index index}] [:> elem-name shape-props]] [:use {:xlinkHref (str "#" shape-id) :clipPath clip-path}]])) - ; The SVG standard does not implement yet the 'stroke-alignment' ; attribute, to define the position of the stroke relative to the ; stroke axis (inner, center, outer). Here we implement a patch to be @@ -254,8 +262,10 @@ (mf/defc shape-custom-stroke {::mf/wrap-props false} [props] + (let [child (obj/get props "children") shape (obj/get props "shape") + index (obj/get props "index") stroke-width (:stroke-width shape 0) stroke-style (:stroke-style shape :none) stroke-position (:stroke-alignment shape :center) @@ -268,11 +278,11 @@ (cond (and has-stroke? inner? closed?) - [:& inner-stroke {:shape shape} + [:& inner-stroke {:shape shape :index index} child] (and has-stroke? outer? closed?) - [:& outer-stroke {:shape shape} + [:& outer-stroke {:shape shape :index index} child] :else diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 33ee5c8be..5a036c82f 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -10,7 +10,6 @@ [app.common.uuid :as uuid] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :as cs] [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fills :as fills] [app.main.ui.shapes.filters :as filters] @@ -69,6 +68,5 @@ (> (count (:fills shape)) 1) (some :fill-color-gradient (:fills shape))) [:& fills/fills {:shape shape :render-id render-id}]) - [:& cs/stroke-defs {:shape shape :render-id render-id}] [:& frame/frame-clip-def {:shape shape :render-id render-id}]] children]])) 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 cd729f181..e41564a95 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -60,7 +60,7 @@ :direction (if (:rtl? data) "rtl" "ltr") :whiteSpace "pre"} (attrs/add-fill data (get-gradient-id index)))})] - [:& shape-custom-stroke {:shape shape} + [:& shape-custom-stroke {:shape shape :index index} [:> :text props (:text data)]]))]])) From a3063eb46dd99d5c9738f27722329e9d91e8d2d4 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 21 Feb 2022 16:21:40 +0100 Subject: [PATCH 14/16] :sparkles: Add support for multiple shapes --- common/src/app/common/pages/common.cljc | 11 +++++++++++ frontend/resources/styles/main/partials/texts.scss | 1 - .../ui/workspace/sidebar/options/shapes/multiple.cljs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index bcab57685..70e17e37b 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -250,6 +250,17 @@ :fill-color-ref-file :fill-color-gradient + :stroke-style + :stroke-alignment + :stroke-width + :stroke-color + :stroke-color-ref-id + :stroke-color-ref-file + :stroke-opacity + :stroke-color-gradient + :stroke-cap-start + :stroke-cap-end + :shadow :blur diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index 41b258e15..0bece2924 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -1,4 +1,3 @@ - .text-editor, .rich-text { color: $color-black; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index b99b757da..5b1d31e95 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -64,7 +64,7 @@ :fill :text :shadow :shape :blur :shape - :stroke :ignore + :stroke :shape :text :text} :image From ec63d236661df9255e64531f0f73d65f0d700a33 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 22 Feb 2022 18:06:48 +0100 Subject: [PATCH 15/16] :sparkles: Multiple fills in text shapes --- common/src/app/common/spec/shape.cljc | 6 +- common/src/app/common/text.cljc | 4 +- .../main/partials/dashboard-header.scss | 2 +- .../src/app/main/data/workspace/colors.cljs | 78 ++++++++------- .../src/app/main/data/workspace/texts.cljs | 79 ++++++++++++--- frontend/src/app/main/ui/shapes/attrs.cljs | 7 +- frontend/src/app/main/ui/shapes/fills.cljs | 87 +++++++++-------- frontend/src/app/main/ui/shapes/mask.cljs | 2 +- frontend/src/app/main/ui/shapes/shape.cljs | 6 +- .../src/app/main/ui/shapes/text/fo_text.cljs | 5 +- .../src/app/main/ui/shapes/text/styles.cljs | 7 +- .../src/app/main/ui/shapes/text/svg_text.cljs | 3 +- .../app/main/ui/workspace/shapes/text.cljs | 15 +-- .../main/ui/workspace/shapes/text/editor.cljs | 6 +- .../workspace/sidebar/options/menus/fill.cljs | 96 +++++-------------- .../workspace/sidebar/options/menus/text.cljs | 2 - .../sidebar/options/shapes/text.cljs | 25 ++--- frontend/src/app/util/text_editor.cljs | 7 ++ frontend/src/app/util/text_svg_position.cljs | 4 +- 19 files changed, 230 insertions(+), 211 deletions(-) diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc index 54853d259..45239f83e 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/spec/shape.cljc @@ -202,9 +202,9 @@ (s/def :internal.shape.text/content (s/nilable (s/or :text-container - (s/keys :req-un [:internal.shape.text/type - :internal.shape.text/children] - :opt-un [:internal.shape.text/key]) + (s/keys :req-un [:internal.shape.text/type] + :opt-un [:internal.shape.text/key + :internal.shape.text/children]) :text-content (s/keys :req-un [:internal.shape.text/text])))) diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index a48f525d4..b58f6b79a 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -26,8 +26,8 @@ :text-transform "none" :text-align "left" :text-decoration "none" - :fill-color clr/black - :fill-opacity 1}) + :fills [{:fill-color clr/black + :fill-opacity 1}]}) (def typography-fields [:font-id diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 4027e6de3..911e67c00 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -110,7 +110,7 @@ .dashboard-buttons { display: flex; - justify-content: end; + justify-content: flex-end; align-items: center; } diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 4ee3fee66..af28baa57 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -113,81 +113,89 @@ (defn transform-fill [state ids color transform] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [objects (wsh/lookup-page-objects state) is-text? #(= :text (:type (get objects %))) text-ids (filter is-text? ids) - shape-ids (filter (comp not is-text?) ids) + shape-ids (remove is-text? ids) - attrs (cond-> {:fill-color nil - :fill-color-gradient nil - :fill-color-ref-file nil - :fill-color-ref-id nil - :fill-opacity nil} + attrs + (cond-> {} + (contains? color :color) + (assoc :fill-color (:color color)) - (contains? color :color) - (assoc :fill-color (:color color)) + (contains? color :id) + (assoc :fill-color-ref-id (:id color)) - (contains? color :id) - (assoc :fill-color-ref-id (:id color)) + (contains? color :file-id) + (assoc :fill-color-ref-file (:file-id color)) - (contains? color :file-id) - (assoc :fill-color-ref-file (:file-id color)) + (contains? color :gradient) + (assoc :fill-color-gradient (:gradient color)) - (contains? color :gradient) - (assoc :fill-color-gradient (:gradient color)) + (contains? color :opacity) + (assoc :fill-opacity (:opacity color)) - (contains? color :opacity) - (assoc :fill-opacity (:opacity color))) - ;; Not nil attrs - clean-attrs (d/without-nils attrs)] + :always + (d/without-nils)) + + transform-attrs #(transform % attrs)] (rx/concat - (rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids)) - (rx/of (dch/update-shapes - shape-ids - #(transform % clean-attrs)))))) + (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) + (rx/of (dch/update-shapes shape-ids transform-attrs))))) (defn swap-fills [shape index new-index] (let [first (get-in shape [:fills index]) second (get-in shape [:fills new-index])] (-> shape (assoc-in [:fills index] second) - (assoc-in [:fills new-index] first)) - )) + (assoc-in [:fills new-index] first)))) (defn reorder-fills [ids index new-index] (ptk/reify ::reorder-fills ptk/WatchEvent - (watch [_ _ _] - (rx/of (dch/update-shapes - ids - #(swap-fills % index new-index)))))) + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + + is-text? #(= :text (:type (get objects %))) + text-ids (filter is-text? ids) + shape-ids (remove is-text? ids) + transform-attrs #(swap-fills % index new-index)] + + (rx/concat + (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) + (rx/of (dch/update-shapes shape-ids transform-attrs))))))) (defn change-fill [ids color position] (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state _] - (let [change (fn [shape attrs] (assoc-in shape [:fills position] (into {} attrs)))] - (transform-fill state ids color change))))) + (let [change (fn [shape attrs] + (-> shape + (cond-> (not (contains? shape :fills)) + (assoc :fills [])) + (assoc-in [:fills position] (into {} attrs))))] + (transform-fill state ids color change))))) (defn change-fill-and-clear [ids color] (ptk/reify ::change-fill-and-clear ptk/WatchEvent (watch [_ state _] - (let [set (fn [shape attrs] (assoc shape :fills [attrs]))] - (transform-fill state ids color set))))) + (let [set (fn [shape attrs] (assoc shape :fills [attrs]))] + (transform-fill state ids color set))))) (defn add-fill [ids color] (ptk/reify ::add-fill ptk/WatchEvent (watch [_ state _] - (let [add (fn [shape attrs] (assoc shape :fills (into [attrs] (:fills shape))))] + (let [add (fn [shape attrs] + (-> shape + (update :fills #(into [attrs] %))))] (transform-fill state ids color add))))) (defn remove-fill diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index c643335ba..1cc798798 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -50,12 +50,14 @@ (update state :workspace-editor-state dissoc id))))) (defn finalize-editor-state - [{:keys [id] :as shape}] + [id] (ptk/reify ::finalize-editor-state ptk/WatchEvent (watch [_ state _] (when (dwc/initialized? state) - (let [content (-> (get-in state [:workspace-editor-state id]) + (let [objects (wsh/lookup-page-objects state) + shape (get objects id) + content (-> (get-in state [:workspace-editor-state id]) (ted/get-editor-current-content))] (if (ted/content-has-text? content) (let [content (d/merge (ted/export-content content) @@ -78,8 +80,8 @@ ptk/UpdateEvent (update [_ state] (let [text-state (some->> content ted/import-content) - attrs (get-in state [:workspace-local :defaults :font]) - + attrs (d/merge txt/default-text-attrs + (get-in state [:workspace-local :defaults :font])) editor (cond-> (ted/create-editor-state text-state decorator) (and (nil? content) (some? attrs)) (ted/update-editor-current-block-data attrs))] @@ -95,7 +97,7 @@ (rx/filter (ptk/type? ::rt/navigate) stream) (rx/filter #(= ::finalize-editor-state %) stream)) (rx/take 1) - (rx/map #(finalize-editor-state shape)))))) + (rx/map #(finalize-editor-state id)))))) (defn select-all "Select all content of the current editor. When not editor found this @@ -145,10 +147,10 @@ ;; --- TEXT EDITION IMPL -(defn- update-shape - [shape pred-fn merge-fn attrs] - (let [merge-attrs #(merge-fn % attrs) - transform #(txt/transform-nodes pred-fn merge-attrs %)] +(defn- update-text-content + [shape pred-fn update-fn attrs] + (let [update-attrs #(update-fn % attrs) + transform #(txt/transform-nodes pred-fn update-attrs %)] (-> shape (update :content transform)))) @@ -160,7 +162,11 @@ (let [objects (wsh/lookup-page-objects state) shape (get objects id) - update-fn #(update-shape % txt/is-root-node? attrs/merge attrs) + update-fn + (fn [shape] + (if (some? (:content shape)) + (update-text-content shape txt/is-root-node? attrs/merge attrs) + (assoc shape :content (attrs/merge {:type "root"} attrs)))) shape-ids (cond (cph/text-shape? shape) [id] (cph/group-shape? shape) (cph/get-children-ids objects id))] @@ -187,7 +193,7 @@ node attrs)) - update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs) + update-fn #(update-text-content % txt/is-paragraph-node? merge-fn attrs) shape-ids (cond (cph/text-shape? shape) [id] (cph/group-shape? shape) (cph/get-children-ids objects id))] @@ -209,12 +215,57 @@ update-node? (fn [node] (or (txt/is-text-node? node) (txt/is-paragraph-node? node))) - - update-fn #(update-shape % update-node? attrs/merge attrs) shape-ids (cond (cph/text-shape? shape) [id] (cph/group-shape? shape) (cph/get-children-ids objects id))] - (rx/of (dch/update-shapes shape-ids update-fn))))))) + (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? attrs/merge attrs)))))))) + +(defn migrate-content + [content] + (txt/transform-nodes + #(or (txt/is-text-node? %) (txt/is-paragraph-node? %)) + (fn [node] + (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] + (cond-> node + (d/not-empty? color-attrs) + (-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient) + (assoc :fills [color-attrs]))))) + content)) + +(defn update-text-with-function + [id update-node-fn] + (ptk/reify ::update-text-with-function + ptk/UpdateEvent + (update [_ state] + (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn update-node-fn)) + + ptk/WatchEvent + (watch [_ state _] + (when (nil? (get-in state [:workspace-editor-state id])) + (let [objects (wsh/lookup-page-objects state) + shape (get objects id) + + update-node? + (fn [node] + (or (txt/is-text-node? node) + (txt/is-paragraph-node? node))) + + shape-ids + (cond + (cph/text-shape? shape) [id] + (cph/group-shape? shape) (cph/get-children-ids objects id)) + + update-content + (fn [content] + (->> content + (migrate-content) + (txt/transform-nodes update-node? update-node-fn))) + + update-shape + (fn [shape] + (d/update-when shape :content update-content))] + + (rx/of (dch/update-shapes shape-ids update-shape))))))) ;; --- RESIZE UTILS diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index c969e2f4e..d1abb8d81 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -205,7 +205,7 @@ (= :image (:type shape)) (> (count (:fills shape)) 1) (some #(some? (:fill-color-gradient %)) (:fills shape))) - (obj/set! styles "fill" (str "url(#fill-" render-id ")")) + (obj/set! styles "fill" (str "url(#fill-0-" render-id ")")) ;; imported svgs can have fill and fill-opacity attributes (obj/contains? svg-styles "fill") @@ -227,9 +227,8 @@ (add-style-attrs shape))) (defn extract-fill-attrs - [shape index] - (let [render-id (mf/use-ctx muc/render-ctx) - fill-styles (-> (obj/get shape "style" (obj/new)) + [shape render-id index] + (let [fill-styles (-> (obj/get shape "style" (obj/new)) (add-fill shape render-id index))] (-> (obj/new) (obj/set! "style" fill-styles)))) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index 5012c1ae9..f6f8f67a8 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -20,45 +20,54 @@ [props] (let [shape (obj/get props "shape") - render-id (obj/get props "render-id") - {:keys [x y width height]} (:selrect shape) - {:keys [metadata]} shape - fill-id (str "fill-" render-id) - has-image (or metadata (:fill-image shape)) - uri (if metadata - (cfg/resolve-file-media metadata) - (cfg/resolve-file-media (:fill-image shape))) - embed (embed/use-data-uris [uri]) - transform (gsh/transform-matrix shape) - pattern-attrs (cond-> #js {:id fill-id - :patternUnits "userSpaceOnUse" - :x x - :y y - :height height - :width width - :data-loading (str (not (contains? embed uri)))} - (= :path (:type shape)) - (obj/set! "patternTransform" transform))] + render-id (obj/get props "render-id")] - [:* - (for [[index value] (-> (d/enumerate (:fills shape [])) reverse)] - (cond (some? (:fill-color-gradient value)) - (case (:type (:fill-color-gradient value)) - :linear [:> grad/linear-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index) - :gradient (:fill-color-gradient value) - :shape shape}] - :radial [:> grad/radial-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index) - :gradient (:fill-color-gradient value) - :shape shape}]))) + (when (or (some? (:fill-image shape)) + (#{:image :text} (:type shape)) + (> (count (:fills shape)) 1) + (some :fill-color-gradient (:fills shape))) - [:> :pattern pattern-attrs - [:g - (for [[index value] (-> (d/enumerate (:fills shape [])) reverse)] - [:> :rect (-> (attrs/extract-fill-attrs value index) - (obj/set! "width" width) - (obj/set! "height" height))]) + (let [{:keys [x y width height]} (:selrect shape) + {:keys [metadata]} shape + + has-image (or metadata (:fill-image shape)) + uri (if metadata + (cfg/resolve-file-media metadata) + (cfg/resolve-file-media (:fill-image shape))) + embed (embed/use-data-uris [uri]) + transform (gsh/transform-matrix shape) + pattern-attrs (cond-> #js {:patternUnits "userSpaceOnUse" + :x x + :y y + :height height + :width width + :data-loading (str (not (contains? embed uri)))} + (= :path (:type shape)) + (obj/set! "patternTransform" transform))] - (when has-image - [:image {:xlinkHref (get embed uri uri) - :width width - :height height}])]]])) + [:* + (for [[_shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] + (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] + (cond (some? (:fill-color-gradient value)) + (case (d/name (:type (:fill-color-gradient value))) + "linear" [:> grad/linear-gradient #js {:id (str "fill-color-gradient_" render-id "_" fill-index) + :gradient (:fill-color-gradient value) + :shape shape}] + "radial" [:> grad/radial-gradient #js {:id (str "fill-color-gradient_" render-id "_" fill-index) + :gradient (:fill-color-gradient value) + :shape shape}])))) + + (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] + (let [fill-id (str "fill-" shape-index "-" render-id)] + [:> :pattern (-> (obj/clone pattern-attrs) + (obj/set! "id" fill-id)) + [:g + (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] + [:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index) + (obj/set! "width" width) + (obj/set! "height" height))]) + + (when has-image + [:image {:xlinkHref (get embed uri uri) + :width width + :height height}])]]))])))) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index a9b58db4c..9a0470901 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -37,7 +37,7 @@ (fn [data] (-> data (dissoc :fill-color :fill-opacity :fill-color-gradient) - (assoc :fill-color "#FFFFFF" :fill-opacity 1)))] + (assoc :fills [{:fill-color "#FFFFFF" :fill-opacity 1}])))] (-> shape (d/update-when :position-data #(mapv update-color %)) (assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 5a036c82f..d81e98606 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -63,10 +63,6 @@ [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] - (when (or (some? (:fill-image shape)) - (= :image (:type shape)) - (> (count (:fills shape)) 1) - (some :fill-color-gradient (:fills shape))) - [:& fills/fills {:shape shape :render-id render-id}]) + [:& fills/fills {:shape shape :render-id render-id}] [:& frame/frame-clip-def {:shape shape :render-id render-id}]] children]])) 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 eb39b1edf..52f9915bb 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -194,13 +194,16 @@ 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) + [colors color-mapping color-mapping-inverse] (retrieve-colors shape) plain-colors? (mf/use-ctx muc/text-plain-colors-ctx) content (cond-> content plain-colors? - (remap-colors color-mapping))] + (remap-colors color-mapping)) + + ] [:foreignObject {:x x diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index e23225338..9f4dda7c3 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -88,9 +88,10 @@ :overflowWrap "initial"} 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))] + (obj/set! "--fills" (transit/encode-str (:fills data))) + #_(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 e41564a95..26731e181 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -59,7 +59,8 @@ :fontStyle (:font-style data) :direction (if (:rtl? data) "rtl" "ltr") :whiteSpace "pre"} - (attrs/add-fill data (get-gradient-id index)))})] + (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")) + #_(attrs/add-fill data (get-gradient-id index)))})] [:& shape-custom-stroke {:shape shape :index 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 6e035fb94..8bff9737b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -155,6 +155,11 @@ show-svg-text? (or (some? position-data) (some? @local-position-data)) + shape + (cond-> shape + (some? @local-position-data) + (assoc :position-data @local-position-data)) + update-position-data (fn [] (when (some? @local-position-data) @@ -171,7 +176,7 @@ (fn [] ;; Timer to update the shape. We do this so a lot of changes won't produce ;; a lot of updates (kind of a debounce) - (let [sid (timers/schedule 100 update-position-data)] + (let [sid (timers/schedule 50 update-position-data)] (fn [] (rx/dispose! sid))))) @@ -217,9 +222,5 @@ :key (str id edition?)}]] (when show-svg-text? - (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}]]))]])) + [: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 5d4b45552..52139e778 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -210,7 +210,11 @@ {:ref self-ref :style {:cursor (cur/text (:rotation shape)) :width (:width shape) - :height (:height shape)} + :height (:height shape) + ;; We hide the editor when is blurred because otherwise the selection won't let us see + ;; the underlying text. Use opacity because display or visibility won't allow to recover + ;; focus afterwards. + :opacity (when @blurred 0)} :on-click on-click :class (dom/classnames :align-top (= (:vertical-align content "top") "top") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index ba0595fff..8cda5adfa 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.sidebar.options.menus.fill (:require - [app.common.attrs :as attrs] [app.common.colors :as clr] [app.common.data :as d] [app.common.pages :as cp] @@ -17,7 +16,6 @@ [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] - [cuerdas.core :as str] [rumext.alpha :as mf])) (def fill-attrs @@ -51,40 +49,6 @@ ;; Excluding nil values values (d/without-nils values) - only-shapes? (and (contains? values :fills) - ;; texts have :fill-* attributes, the rest of the shapes have :fills - (= (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0)) - - shapes-and-texts? (and (contains? values :fills) - ;; texts have :fill-* attributes, the rest of the shapes have :fills - (> (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0)) - - ;; Texts still have :fill-* attributes and the rest of the shapes just :fills so we need some extra calculation when multiple selection happens to detect them - plain-values (if (vector? (:fills values)) - (concat (:fills values) [(dissoc values :fills)]) - values) - - plain-values (attrs/get-attrs-multi plain-values [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient]) - - plain-values (if (empty? plain-values) - values - plain-values) - - ;; We must control some rare situations like - ;; - Selecting texts and shapes with different fills - ;; - Selecting a text and a shape with empty fills - plain-values (if (and shapes-and-texts? - (or - (= (:fills values) :multiple) - (= 0 (count (:fills values))))) - {:fills :multiple - :fill-color :multiple - :fill-opacity :multiple - :fill-color-ref-id :multiple - :fill-color-ref-file :multiple - :fill-color-gradient :multiple} - plain-values) - hide-fill-on-export? (:hide-fill-on-export values false) checkbox-ref (mf/use-ref) @@ -110,12 +74,6 @@ (fn [index] (st/emit! (dc/reorder-fills ids index new-index))))) - on-change-mixed-shapes - (mf/use-callback - (mf/deps ids) - (fn [color] - (st/emit! (dc/change-fill-and-clear ids color)))) - on-remove (fn [index] (fn [] @@ -155,43 +113,33 @@ [:div.element-set [:div.element-set-title [:span label] - (when (and (not disable-remove?) (not (= :multiple (:fills values))) only-shapes?) + (when (and (not disable-remove?) (not (= :multiple (:fills values)))) [:div.add-page {:on-click on-add} i/close])] [:div.element-set-content - (if only-shapes? - (cond - (= :multiple (:fills values)) - [:div.element-set-options-group - [:div.element-set-label (tr "settings.multiple")] - [:div.element-set-actions - [:div.element-set-actions-button {:on-click on-remove-all} - i/minus]]] + (cond + (= :multiple (:fills values)) + [:div.element-set-options-group + [:div.element-set-label (tr "settings.multiple")] + [:div.element-set-actions + [:div.element-set-actions-button {:on-click on-remove-all} + i/minus]]] - (seq (:fills values)) - [:& h/sortable-container {} - (for [[index value] (d/enumerate (:fills values []))] - [:& color-row {:color {:color (:fill-color value) - :opacity (:fill-opacity value) - :id (:fill-color-ref-id value) - :file-id (:fill-color-ref-file value) - :gradient (:fill-color-gradient value)} - :index index - :title (tr "workspace.options.fill") - :on-change (on-change index) - :on-reorder (on-reorder index) - :on-detach (on-detach index) - :on-remove (on-remove index)}])]) - - [:& color-row {:color {:color (:fill-color plain-values) - :opacity (:fill-opacity plain-values) - :id (:fill-color-ref-id plain-values) - :file-id (:fill-color-ref-file plain-values) - :gradient (:fill-color-gradient plain-values)} - :title (tr "workspace.options.fill") - :on-change on-change-mixed-shapes - :on-detach (on-detach 0)}]) + (seq (:fills values)) + [:& h/sortable-container {} + (for [[index value] (d/enumerate (:fills values []))] + [:& color-row {:color {:color (:fill-color value) + :opacity (:fill-opacity value) + :id (:fill-color-ref-id value) + :file-id (:fill-color-ref-file value) + :gradient (:fill-color-gradient value)} + :index index + :title (tr "workspace.options.fill") + :on-change (on-change index) + :on-reorder (on-reorder index) + :on-detach (on-detach index) + :on-remove (on-remove index)}])]) (when (or (= type :frame) (and (= type :multiple) (some? hide-fill-on-export?))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 24c98f971..fd0c3502d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -347,5 +347,3 @@ [:div.row-flex [:> grow-options opts] [:div.align-icons]]]])) - - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index fa1990b2a..8e7803c1c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -6,7 +6,6 @@ (ns app.main.ui.workspace.sidebar.options.shapes.text (:require - [app.common.colors :as clr] [app.common.data :as d] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] @@ -30,19 +29,16 @@ layer-values (select-keys shape layer-attrs) - fill-values (dwt/current-text-values - {:editor-state editor-state - :shape shape - :attrs text-fill-attrs}) + fill-values (-> (dwt/current-text-values + {:editor-state editor-state + :shape shape + :attrs (conj text-fill-attrs :fills)}) + (d/update-in-when [:fill-color-gradient :type] keyword)) - fill-values (d/update-in-when fill-values [:fill-color-gradient :type] keyword) - - fill-values (cond-> fill-values - (not (contains? fill-values :fill-color)) (assoc :fill-color clr/black) - (not (contains? fill-values :fill-opacity)) (assoc :fill-opacity 1) - ;; Keep for backwards compatibility - (:fill fill-values) (assoc :fill-color (:fill fill-values)) - (:opacity fill-values) (assoc :fill-opacity (:fill fill-values))) + fill-values (if (not (contains? fill-values :fills)) + ;; Old fill format + {:fills [fill-values]} + fill-values) stroke-values (select-keys shape stroke-attrs) @@ -79,8 +75,7 @@ [:& fill-menu {:ids ids :type type - :values fill-values - :disable-remove? true}] + :values fill-values}] [:& stroke-menu {:ids ids :type type diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index f1d0a7735..5cf866a9e 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -95,6 +95,13 @@ selected (impl/getSelectedBlocks state)] (reduce update-blocks state selected))) +(defn update-editor-current-inline-styles-fn + [state update-fn] + (let [attrs (-> (.getCurrentInlineStyle ^js state) + (txt/styles-to-attrs) + (update-fn))] + (impl/applyInlineStyle state (txt/attrs-to-styles attrs)))) + (defn editor-split-block [state] (impl/splitBlockPreservingData state)) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index ee9a0f0ba..5e62e3e82 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -119,7 +119,5 @@ :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")) + :fills (transit/decode-str (get "--fills")) :text text})))))))))) From 64ffa9bb3f4ebe68a2a7e5ea371deb45120a37ba Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 24 Feb 2022 14:04:43 +0100 Subject: [PATCH 16/16] :bug: Fix problems with old texts --- common/src/app/common/spec/shape.cljc | 4 +- .../src/app/main/data/workspace/texts.cljs | 58 +++++++++++++------ .../app/main/ui/hooks/mutable_observer.cljs | 11 ++-- frontend/src/app/main/ui/render.cljs | 10 ++-- frontend/src/app/main/ui/shapes/attrs.cljs | 4 +- frontend/src/app/main/ui/shapes/fills.cljs | 23 ++++---- .../src/app/main/ui/shapes/text/styles.cljs | 21 +++++-- .../src/app/main/ui/shapes/text/svg_text.cljs | 5 +- .../app/main/ui/workspace/shapes/text.cljs | 33 ++++++----- frontend/src/app/util/text_svg_position.cljs | 15 ++--- 10 files changed, 107 insertions(+), 77 deletions(-) diff --git a/common/src/app/common/spec/shape.cljc b/common/src/app/common/spec/shape.cljc index 45239f83e..ce152d0a4 100644 --- a/common/src/app/common/spec/shape.cljc +++ b/common/src/app/common/spec/shape.cljc @@ -224,7 +224,7 @@ :internal.shape.text.position-data/font-size :internal.shape.text.position-data/font-style :internal.shape.text.position-data/font-weight - :internal.shape.text.position-data/rtl? + :internal.shape.text.position-data/rtl :internal.shape.text.position-data/text :internal.shape.text.position-data/text-decoration :internal.shape.text.position-data/text-transform] @@ -243,7 +243,7 @@ (s/def :internal.shape.text.position-data/font-size string?) (s/def :internal.shape.text.position-data/font-style string?) (s/def :internal.shape.text.position-data/font-weight string?) -(s/def :internal.shape.text.position-data/rtl? boolean?) +(s/def :internal.shape.text.position-data/rtl boolean?) (s/def :internal.shape.text.position-data/text string?) (s/def :internal.shape.text.position-data/text-decoration string?) (s/def :internal.shape.text.position-data/text-transform string?) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 1cc798798..7b0b5f4c5 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -117,13 +117,31 @@ ;; --- Helpers +(defn to-new-fills + [data] + [(d/without-nils (select-keys data [:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file]))]) + (defn- shape-current-values [shape pred attrs] (let [root (:content shape) nodes (->> (txt/node-seq pred root) - (map #(if (txt/is-text-node? %) - (merge txt/default-text-attrs %) - %)))] + (map (fn [node] + (if (txt/is-text-node? node) + (let [fills + (cond + (or (some? (:fill-color node)) + (some? (:fill-opacity node)) + (some? (:fill-color-gradient node))) + (to-new-fills node) + + (some? (:fills node)) + (:fills node) + + :else + (:fills txt/default-text-attrs))] + (-> (merge txt/default-text-attrs node) + (assoc :fills fills))) + node))))] (attrs/get-attrs-multi nodes attrs))) (defn current-root-values @@ -140,8 +158,10 @@ (defn current-text-values [{:keys [editor-state attrs shape]}] (if editor-state - (-> (ted/get-editor-current-inline-styles editor-state) - (select-keys attrs)) + (let [result (-> (ted/get-editor-current-inline-styles editor-state) + (select-keys attrs)) + result (if (empty? result) txt/default-text-attrs result)] + result) (shape-current-values shape txt/is-text-node? attrs))) @@ -220,24 +240,27 @@ (cph/group-shape? shape) (cph/get-children-ids objects id))] (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? attrs/merge attrs)))))))) +(defn migrate-node + [node] + (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] + (cond-> node + (d/not-empty? color-attrs) + (-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient) + (assoc :fills [color-attrs])) + + (nil? (:fills node)) + (assoc :fills (:fills txt/default-text-attrs))))) + (defn migrate-content [content] - (txt/transform-nodes - #(or (txt/is-text-node? %) (txt/is-paragraph-node? %)) - (fn [node] - (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] - (cond-> node - (d/not-empty? color-attrs) - (-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient) - (assoc :fills [color-attrs]))))) - content)) + (txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content)) (defn update-text-with-function [id update-node-fn] (ptk/reify ::update-text-with-function ptk/UpdateEvent (update [_ state] - (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn update-node-fn)) + (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node))) ptk/WatchEvent (watch [_ state _] @@ -245,10 +268,7 @@ (let [objects (wsh/lookup-page-objects state) shape (get objects id) - update-node? - (fn [node] - (or (txt/is-text-node? node) - (txt/is-paragraph-node? node))) + update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?) shape-ids (cond diff --git a/frontend/src/app/main/ui/hooks/mutable_observer.cljs b/frontend/src/app/main/ui/hooks/mutable_observer.cljs index f9852fea9..b3dabaf45 100644 --- a/frontend/src/app/main/ui/hooks/mutable_observer.cljs +++ b/frontend/src/app/main/ui/hooks/mutable_observer.cljs @@ -48,11 +48,10 @@ (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))))) + (mf/with-effect + (fn [] + (when-let [^js prev-obs (mf/ref-val prev-obs-ref)] + (.disconnect prev-obs) + (mf/set-ref-val! prev-obs-ref nil)))) [node-ref set-node])) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index b5dc467a1..af1a5ae66 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.render (:require + [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -92,12 +93,11 @@ (mf/with-memo [objects] (render/shape-wrapper-factory objects)) - text-shapes - (->> objects - (filter (fn [[_ shape]] (= :text (:type shape)))) - (mapv second)) + is-text? (fn [shape] (= :text (:type shape))) - render-texts? (and render-texts? (some #(nil? (:position-data %)) text-shapes))] + text-shapes (sequence (comp (map second) (filter is-text?)) objects) + + render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] (mf/with-effect [width height] (dom/set-page-style {:size (str (mth/ceil width) "px " diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index d1abb8d81..60370404d 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -89,11 +89,11 @@ (cond (contains? shape :fill-image) (let [fill-image-id (str "fill-image-" render-id)] - {:fill (str/format "url(#%s)" fill-image-id)}) + {:fill (str "url(#" fill-image-id ")")}) (contains? shape :fill-color-gradient) (let [fill-color-gradient-id (str "fill-color-gradient_" render-id (if index (str "_" index) ""))] - {:fill (str/format "url(#%s)" fill-color-gradient-id)}) + {:fill (str "url(#" fill-color-gradient-id ")")}) (contains? shape :fill-color) {:fill (:fill-color shape)} diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index f6f8f67a8..ab98cf852 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -45,19 +45,18 @@ (= :path (:type shape)) (obj/set! "patternTransform" transform))] - [:* - (for [[_shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] + (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] + [:* (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] - (cond (some? (:fill-color-gradient value)) - (case (d/name (:type (:fill-color-gradient value))) - "linear" [:> grad/linear-gradient #js {:id (str "fill-color-gradient_" render-id "_" fill-index) - :gradient (:fill-color-gradient value) - :shape shape}] - "radial" [:> grad/radial-gradient #js {:id (str "fill-color-gradient_" render-id "_" fill-index) - :gradient (:fill-color-gradient value) - :shape shape}])))) + (when (some? (:fill-color-gradient value)) + (let [props #js {:id (str "fill-color-gradient_" render-id "_" fill-index) + :gradient (:fill-color-gradient value) + :shape shape}] + (case (d/name (:type (:fill-color-gradient value))) + "linear" [:> grad/linear-gradient props] + "radial" [:> grad/radial-gradient props])))) + - (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] (let [fill-id (str "fill-" shape-index "-" render-id)] [:> :pattern (-> (obj/clone pattern-attrs) (obj/set! "id" fill-id)) @@ -70,4 +69,4 @@ (when has-image [:image {:xlinkHref (get embed uri uri) :width width - :height height}])]]))])))) + :height height}])]])]))))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 9f4dda7c3..232aa2acd 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -87,11 +87,22 @@ :caretColor (or text-color "black") :overflowWrap "initial"} - base (-> base - (obj/set! "--fills" (transit/encode-str (:fills data))) - #_(obj/set! "--fill-color" fill-color) - #_(obj/set! "--fill-color-gradient" (transit/encode-str (:fill-color-gradient data))) - #_(obj/set! "--fill-opacity" fill-opacity))] + fills + (cond + (some? (:fills data)) + (:fills data) + + (or (some? (:fill-color data)) + (some? (:fill-opacity data)) + (some? (:fill-color-gradient data))) + [(d/without-nils (select-keys data [:fill-color :fill-opacity :fill-color-gradient :fill-color-ref-id :fill-color-ref-file]))] + + (nil? (:fills data)) + [{:fill-color "#000000" :fill-opacity 1}]) + + base (cond-> base + (some? fills) + (obj/set! "--fills" (transit/encode-str fills)))] (when (and (string? letter-spacing) (pos? (alength letter-spacing))) 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 26731e181..6bb26816b 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -57,10 +57,9 @@ :textTransform (:text-transform data) :textDecoration (:text-decoration data) :fontStyle (:font-style data) - :direction (if (:rtl? data) "rtl" "ltr") + :direction (if (:rtl data) "rtl" "ltr") :whiteSpace "pre"} - (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")) - #_(attrs/add-fill data (get-gradient-id index)))})] + (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))})] [:& shape-custom-stroke {:shape shape :index 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 8bff9737b..18bf8fc09 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -131,25 +131,26 @@ sid-ref (mf/use-ref nil) handle-change-foreign-object - (fn [node] - (when-let [position-data (utp/calc-position-data node)] - (let [parent (dom/get-parent node) - parent-transform (dom/get-attribute parent "transform") - node-transform (dom/get-attribute node "transform") + (mf/use-callback + (fn [node] + (when-let [position-data (utp/calc-position-data node)] + (let [parent (dom/get-parent node) + parent-transform (dom/get-attribute parent "transform") + node-transform (dom/get-attribute node "transform") - parent-mtx (usvg/parse-transform parent-transform) - node-mtx (usvg/parse-transform node-transform) + parent-mtx (usvg/parse-transform parent-transform) + node-mtx (usvg/parse-transform node-transform) - ;; We need to see what transformation is applied in the DOM to reverse it - ;; before calculating the position data - mtx (-> (gmt/multiply parent-mtx node-mtx) - (gmt/inverse)) + ;; We need to see what transformation is applied in the DOM to reverse it + ;; before calculating the position data + mtx (-> (gmt/multiply parent-mtx node-mtx) + (gmt/inverse)) - position-data - (->> position-data - (mapv #(merge % (-> (select-keys % [:x :y :width :height]) - (gsh/transform-rect mtx)))))] - (reset! local-position-data position-data)))) + position-data + (->> position-data + (mapv #(merge % (-> (select-keys % [:x :y :width :height]) + (gsh/transform-rect mtx)))))] + (reset! local-position-data position-data))))) [node-ref on-change-node] (use-mutable-observer handle-change-foreign-object) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index 5e62e3e82..b8a8a4f94 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -22,9 +22,10 @@ (.setEnd range node end-i) (.getClientRects range))) +;; TODO: Evaluate to change this function to Javascript (defn parse-text-nodes "Given a text node retrieves the rectangles for everyone of its paragraphs and its text." - [parent-node rtl? text-node] + [parent-node rtl text-node] (let [content (.-textContent text-node) text-size (.-length content)] @@ -45,7 +46,7 @@ ;; If the rects increase means we're in a new paragraph (if (> (.-length rects) 1) (let [entry {:node parent-node - :position (dom/bounding-rect->rect (if rtl? (second rects) (first rects))) + :position (dom/bounding-rect->rect (if rtl (second rects) (first rects))) :text current}] (recur to-i to-i "" (conj result entry))) (recur from-i (inc to-i) (str current (nth content to-i)) result))))))) @@ -86,9 +87,9 @@ (->> text-nodes (mapcat (fn [parent-node] - (let [rtl? (= "rtl" (.-dir (.-parentElement parent-node)))] + (let [rtl (= "rtl" (.-dir (.-parentElement parent-node)))] (->> (.-childNodes parent-node) - (mapcat #(parse-text-nodes parent-node rtl? %)))))) + (mapcat #(parse-text-nodes parent-node rtl %)))))) (mapv #(update % :position translate-rect)))))) (defn calc-position-data @@ -101,15 +102,15 @@ (->> text-data (mapv (fn [{:keys [node position text]}] (let [{:keys [x y width height]} position - rtl? (= "rtl" (.-dir (.-parentElement ^js node))) + 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) + {:rtl rtl + :x (if rtl (+ x width) x) :y (+ y height) :width width :height height