0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 00:01:51 -05:00

Allows svg text on test edit and creation

This commit is contained in:
alonso.torres 2022-02-16 16:18:15 +01:00
parent 18dded1a00
commit 6cb6adc134
8 changed files with 198 additions and 123 deletions

View file

@ -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))

View file

@ -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))))

View file

@ -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;

View file

@ -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)))

View file

@ -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)]))]]]))

View file

@ -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}]]))]]))

View file

@ -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)}]]]]))

View file

@ -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})))))))