0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-25 08:16:49 -05:00

Merge pull request #405 from penpot/issues/text-shape

Refactor the text size calculations
This commit is contained in:
Andrey Antukh 2020-11-27 16:19:46 +01:00 committed by GitHub
commit 53297ec9d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1273 additions and 1198 deletions

View file

@ -44,26 +44,29 @@
(move shape (gpt/point dx dy))))
;; --- Resize (Dimensions)
;; Fixme: Improve using modifiers instead of calculating the selrect/points
(defn resize
[shape width height]
(us/assert map? shape)
(us/assert number? width)
(us/assert number? height)
(let [selrect (-> (:selrect shape)
(assoc :width width)
(assoc :height height)
(assoc :x2 (+ (-> shape :selrect :x1) width))
(assoc :y2 (+ (-> shape :selrect :y1) height)))
center (gco/center-selrect selrect)
points (-> selrect gpr/rect->points (gtr/transform-points center (:transform shape)))]
(let [shape-transform (:transform shape (gmt/matrix))
shape-transform-inv (:transform-inverse shape (gmt/matrix))
shape-center (gco/center-shape shape)
{sr-width :width sr-height :height} (:selrect shape)
origin (-> (gpt/point (:selrect shape))
(gtr/transform-point-center shape-center shape-transform))
scalev (gpt/divide (gpt/point width height)
(gpt/point sr-width sr-height))]
(-> shape
(assoc :width width)
(assoc :height height)
(assoc :selrect selrect)
(assoc :points points))))
(update :modifiers assoc
:resize-vector scalev
:resize-origin origin
:resize-transform shape-transform
:resize-transform-inverse shape-transform-inv)
(gtr/transform-shape))))
(defn resize-rect
[shape attr value]
@ -258,7 +261,10 @@
(defn points->selrect [points] (gpr/points->selrect points))
(defn transform-shape [shape] (gtr/transform-shape shape))
(defn transform-matrix [shape] (gtr/transform-matrix shape))
(defn transform-matrix
([shape] (gtr/transform-matrix shape))
([shape options] (gtr/transform-matrix shape options)))
(defn transform-point-center [point center transform] (gtr/transform-point-center point center transform))
(defn transform-rect [rect mtx] (gtr/transform-rect rect mtx))

View file

@ -22,12 +22,17 @@
(defn transform-matrix
"Returns a transformation matrix without changing the shape properties.
The result should be used in a `transform` attribute in svg"
([{:keys [x y] :as shape}]
([shape] (transform-matrix shape nil))
([{:keys [x y flip-x flip-y] :as shape} {:keys [no-flip]}]
(let [shape-center (or (gco/center-shape shape)
(gpt/point 0 0))]
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply (:transform shape (gmt/matrix)))
(cond->
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate shape-center))))))
(defn transform-point-center

View file

@ -44,18 +44,33 @@
(s/def ::component-root? boolean?)
(s/def ::shape-ref uuid?)
(s/def ::safe-integer
#(and
(integer? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def ::safe-integer ::us/safe-integer)
(s/def ::safe-number ::us/safe-number)
(s/def ::safe-number
#(and
(number? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def :internal.matrix/a ::us/safe-number)
(s/def :internal.matrix/b ::us/safe-number)
(s/def :internal.matrix/c ::us/safe-number)
(s/def :internal.matrix/d ::us/safe-number)
(s/def :internal.matrix/e ::us/safe-number)
(s/def :internal.matrix/f ::us/safe-number)
(s/def ::matrix
(s/and (s/keys :req-un [:internal.matrix/a
:internal.matrix/b
:internal.matrix/c
:internal.matrix/d
:internal.matrix/e
:internal.matrix/f])
gmt/matrix?))
(s/def :internal.point/x ::us/safe-number)
(s/def :internal.point/y ::us/safe-number)
(s/def ::point
(s/and (s/keys :req-un [:internal.point/x
:internal.point/y])
gpt/point?))
;; GRADIENTS
@ -252,7 +267,6 @@
(s/def :internal.shape/exports
(s/coll-of :internal.shape/export :kind vector?))
(s/def :internal.shape/selrect
(s/keys :req-un [:internal.shape/x
:internal.shape/y
@ -263,15 +277,15 @@
:internal.shape/width
:internal.shape/height]))
(s/def :internal.shape/point
(s/and (s/keys :req-un [:internal.shape/x :internal.shape/y]) gpt/point?))
(s/def :internal.shape/points
(s/every :internal.shape/point :kind vector?))
(s/every ::point :kind vector?))
(s/def :internal.shape/shapes
(s/every uuid? :kind vector?))
(s/def :internal.shape/transform ::matrix)
(s/def :internal.shape/transform-inverse ::matrix)
(s/def ::shape-attrs
(s/keys :opt-un [:internal.shape/selrect
:internal.shape/points
@ -306,6 +320,8 @@
:internal.shape/stroke-width
:internal.shape/stroke-alignment
:internal.shape/text-align
:internal.shape/transform
:internal.shape/transform-inverse
:internal.shape/width
:internal.shape/height
:internal.shape/interactions
@ -781,7 +797,9 @@
[data {:keys [id page-id component-id operations] :as change}]
(let [update-fn (fn [objects]
(if-let [obj (get objects id)]
(assoc objects id (reduce process-operation obj operations))
(let [result (reduce process-operation obj operations)]
(us/verify ::shape result)
(assoc objects id result))
objects))]
(if page-id
(d/update-in-when data [:pages-index page-id :objects] update-fn)

View file

@ -121,6 +121,21 @@
(s/def ::point gpt/point?)
(s/def ::id ::uuid)
(def max-safe-int 9007199254740991)
(def min-safe-int -9007199254740991)
(s/def ::safe-integer
#(and
(integer? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def ::safe-number
#(and
(number? %)
(>= % min-safe-int)
(<= % max-safe-int)))
;; --- Macros
(defn spec-assert

View file

@ -16,7 +16,7 @@
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2020.08.10-2"}
funcool/promesa {:mvn/version "6.0.0"}
funcool/rumext {:mvn/version "2020.10.14-1"}
funcool/rumext {:mvn/version "2020.11.27-0"}
lambdaisland/uri {:mvn/version "1.4.54"
:exclusions [org.clojure/data.json]}

View file

@ -2,25 +2,28 @@
;; 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) 2016 Andrey Antukh <niwi@niwi.nz>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.media
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.spec :as us]
[app.common.data :as d]
[app.common.media :as cm]
[app.main.data.messages :as dm]
[app.main.store :as st]
[app.main.repo :as rp]
[app.util.i18n :refer [tr]]
[app.util.router :as rt]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.messages :as dm]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[app.util.router :as r]
[app.util.router :as rt]
[app.util.time :as ts]
[app.util.router :as r]))
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]))
;; --- Specs

View file

@ -28,12 +28,12 @@
(s/def ::status #{:visible :hide})
(s/def ::controls #{:none :close :inline-actions :bottom-actions})
(s/def ::tag ::us/string)
(s/def ::tag (s/or :str ::us/string :kw ::us/keyword))
(s/def ::label ::us/string)
(s/def ::callback fn?)
(s/def ::action (s/keys :req-un [::label ::callback]))
(s/def ::actions (s/every ::message-action :kind vector?))
(s/def ::timeout ::us/integer)
(s/def ::timeout (s/nilable ::us/integer))
(s/def ::content ::us/string)
(s/def ::message

View file

@ -136,12 +136,18 @@
(->> data
(filter #(= page-id (:page-id %)))
(d/index-by :id)
(assoc state :comment-threads)))]
(assoc state :comment-threads)))
(on-error [err]
(if (= :authorization (:type err))
(rx/empty)
(rx/throw err)))]
(ptk/reify ::fetch-comment-threads
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/query :comment-threads {:file-id file-id})
(rx/map #(partial fetched %)))))))
(rx/map #(partial fetched %))
(rx/catch on-error))))))
(defn refresh-comment-thread
[{:keys [id file-id] :as thread}]

View file

@ -1014,7 +1014,7 @@
(ptk/reify ::update-dimensions
ptk/WatchEvent
(watch [_ state stream]
(rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value))))))
(rx/of (dwc/update-shapes ids #(gsh/resize-rect % attr value) {:reg-objects? true})))))
;; --- Shape Proportions

View file

@ -66,7 +66,7 @@
commit-local? false}
:as opts}]
(us/verify ::cp/changes changes)
(us/verify ::cp/changes undo-changes)
;; (us/verify ::cp/changes undo-changes)
(ptk/reify ::commit-changes
cljs.core/IDeref
(-deref [_] changes)
@ -382,33 +382,33 @@
(ptk/reify ::update-shapes
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)]
(loop [ids (seq ids)
rch []
uch []]
(if (nil? ids)
(rx/of (commit-changes
(cond-> rch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
(cond-> uch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
{:commit-local? true}))
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)]
(loop [ids (seq ids)
rch []
uch []]
(if (nil? ids)
(rx/of (commit-changes
(cond-> rch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
(cond-> uch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
{:commit-local? true}))
(let [id (first ids)
obj1 (get objects id)
obj2 (f obj1)
rch-operations (generate-operations obj1 obj2)
uch-operations (generate-operations obj2 obj1 true)
rchg {:type :mod-obj
:page-id page-id
:operations rch-operations
:id id}
uchg {:type :mod-obj
:page-id page-id
:operations uch-operations
:id id}]
(recur (next ids)
(if (empty? rch-operations) rch (conj rch rchg))
(if (empty? uch-operations) uch (conj uch uchg)))))))))))
(let [id (first ids)
obj1 (get objects id)
obj2 (f obj1)
rch-operations (generate-operations obj1 obj2)
uch-operations (generate-operations obj2 obj1 true)
rchg {:type :mod-obj
:page-id page-id
:operations rch-operations
:id id}
uchg {:type :mod-obj
:page-id page-id
:operations uch-operations
:id id}]
(recur (next ids)
(if (empty? rch-operations) rch (conj rch rchg))
(if (empty? uch-operations) uch (conj uch uchg)))))))))))
(defn update-shapes-recursive

View file

@ -170,3 +170,8 @@
(or
(d/seek #(or (= (:id %) "regular") (= (:name %) "regular")) variants)
(first variants)))
(defn fetch-font [font-id font-variant-id]
(let [font-url (font-url font-id font-variant-id)]
(-> (js/fetch font-url)
(p/then (fn [res] (.text res))))))

View file

@ -9,201 +9,56 @@
(ns app.main.ui.shapes.text
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.shapes :as geom]
[app.main.data.fetch :as df]
[app.main.fonts :as fonts]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.ui.context :as muc]
[app.main.ui.shapes.group :refer [mask-id-ctx]]
[app.util.color :as uc]
[app.common.data :as d]
[app.common.geom.shapes :as geom]
[app.common.geom.matrix :as gmt]
[app.util.object :as obj]
[app.util.text :as ut]
[clojure.set :as set]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.alpha :as mf]))
;; --- Text Editor Rendering
(defn- generate-root-styles
[data]
(let [valign (obj/get data "vertical-align" "top")
talign (obj/get data "text-align" "flex-start")
base #js {:height "100%"
:width "100%"
:display "flex"}]
(cond-> base
(= valign "top") (obj/set! "alignItems" "flex-start")
(= valign "center") (obj/set! "alignItems" "center")
(= valign "bottom") (obj/set! "alignItems" "flex-end")
(= talign "left") (obj/set! "justifyContent" "flex-start")
(= talign "center") (obj/set! "justifyContent" "center")
(= talign "right") (obj/set! "justifyContent" "flex-end")
(= talign "justify") (obj/set! "justifyContent" "stretch"))))
(defn- generate-paragraph-styles
[data]
(let [base #js {:fontSize "14px"
:margin "inherit"
:lineHeight "1.2"}
lh (obj/get data "line-height")
ta (obj/get data "text-align")]
(cond-> base
ta (obj/set! "textAlign" ta)
lh (obj/set! "lineHeight" lh))))
(defn- generate-text-styles
[data]
(let [letter-spacing (obj/get data "letter-spacing")
text-decoration (obj/get data "text-decoration")
text-transform (obj/get data "text-transform")
line-height (obj/get data "line-height")
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
font-variant-id (obj/get data "font-variant-id")
font-family (obj/get data "font-family")
font-size (obj/get data "font-size")
;; Old properties for backwards compatibility
fill (obj/get data "fill")
opacity (obj/get data "opacity" 1)
fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity)
fill-color-gradient (obj/get data "fill-color-gradient" nil)
fill-color-gradient (when fill-color-gradient
(-> (js->clj fill-color-gradient :keywordize-keys true)
(update :type keyword)))
fill-color-ref-id (obj/get data "fill-color-ref-id")
fill-color-ref-file (obj/get data "fill-color-ref-file")
[r g b a] (uc/hex->rgba fill-color fill-opacity)
background (if fill-color-gradient
(uc/gradient->css (js->clj fill-color-gradient))
(str/format "rgba(%s, %s, %s, %s)" r g b a))
fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration
:textTransform text-transform
:lineHeight (or line-height "inherit")
"--text-color" background}]
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))
(obj/set! base "letterSpacing" (str letter-spacing "px")))
(when (and (string? font-size)
(pos? (alength font-size)))
(obj/set! base "fontSize" (str font-size "px")))
(when (and (string? font-id)
(pos? (alength font-id)))
(let [font (get fontsdb font-id)]
(fonts/ensure-loaded! font-id)
(let [font-family (or (:family font)
(obj/get data "fontFamily"))
font-variant (d/seek #(= font-variant-id (:id %))
(:variants font))
font-style (or (:style font-variant)
(obj/get data "fontStyle"))
font-weight (or (:weight font-variant)
(obj/get data "fontWeight"))]
(obj/set! base "fontFamily" font-family)
(obj/set! base "fontStyle" font-style)
(obj/set! base "fontWeight" font-weight))))
base))
(defn get-all-fonts [node]
(let [current-font (if (not (nil? (:font-id node)))
#{(select-keys node [:font-id :font-variant-id])}
#{})
children-font (map get-all-fonts (:children node))]
(reduce set/union (conj children-font current-font))))
(defn fetch-font [font-id font-variant-id]
(let [font-url (fonts/font-url font-id font-variant-id)]
(-> (js/fetch font-url)
(p/then (fn [res] (.text res))))))
(defonce font-face-template "
/* latin */
@font-face {
font-family: '$0';
font-style: $3;
font-weight: $2;
font-display: block;
src: url(/fonts/%(0)s-$1.woff) format('woff');
}
")
(defn get-local-font-css [font-id font-variant-id]
(let [{:keys [family variants]} (get @fonts/fontsdb font-id)
{:keys [name weight style]} (->> variants (filter #(= (:id %) font-variant-id)) first)
css-str (str/format font-face-template [family name weight style])]
(p/resolved css-str)))
(defn embed-font [{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}]
(let [{:keys [backend]} (get @fonts/fontsdb font-id)]
(p/let [font-text (case backend
:google (fetch-font font-id font-variant-id)
(get-local-font-css font-id font-variant-id))
url-to-data (->> font-text
(re-seq #"url\(([^)]+)\)")
(map second)
(map df/fetch-as-data-uri)
(p/all))]
(reduce (fn [text [url data]] (str/replace text url data)) font-text url-to-data))
))
[app.util.color :as uc]
[app.main.ui.shapes.text.styles :as sts]
[app.main.ui.shapes.text.embed :as ste]))
;; -- Text nodes
(mf/defc text-node
[{:keys [node index] :as props}]
[{:keys [node index shape] :as props}]
(let [embed-resources? (mf/use-ctx muc/embed-ctx)
embeded-fonts (mf/use-state nil)
{:keys [type text children]} node]
{:keys [type text children]} node
(mf/use-effect
(mf/deps node)
(fn []
(when (and embed-resources? (= type "root"))
(let [font-to-embed (get-all-fonts node)
font-to-embed (if (empty? font-to-embed) #{ut/default-text-attrs} font-to-embed)
embeded (map embed-font font-to-embed)]
(-> (p/all embeded)
(p/then (fn [result] (reset! embeded-fonts (str/join "\n" result)))))))))
render-node
(fn [index node]
(mf/element text-node {:index index
:node node
:key index
:shape shape}))]
(if (string? text)
(let [style (generate-text-styles (clj->js node))]
(let [style (sts/generate-text-styles (clj->js node))]
[:span.text-node {:style style} (if (= text "") "\u00A0" text)])
(let [children (map-indexed (fn [index node]
(mf/element text-node {:index index :node node :key index}))
children)]
(let [children (map-indexed render-node children)]
(case type
"root"
(let [style (generate-root-styles (clj->js node))]
(let [style (sts/generate-root-styles (clj->js node) #js {:shape shape})]
[:div.root.rich-text
{:key index
:style style
:xmlns "http://www.w3.org/1999/xhtml"}
[:*
[:style ".text-node { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
(when (not (nil? @embeded-fonts))
[:style @embeded-fonts])]
(when embed-resources?
[ste/embed-fontfaces-style {:node node}])]
children])
"paragraph-set"
(let [style #js {:display "inline-block"}]
[:div.paragraphs {:key index :style style} children])
(let [style (sts/generate-paragraph-set-styles (clj->js node))]
[:div.paragraph-set {:key index :style style} children])
"paragraph"
(let [style (generate-paragraph-styles (clj->js node))]
[:p {:key index :style style} children])
(let [style (sts/generate-paragraph-styles (clj->js node))]
[:p.paragraph {:key index :style style} children])
nil)))))
@ -211,31 +66,37 @@
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [root (obj/get props "content")]
[:& text-node {:index 0 :node root}]))
(let [root (obj/get props "content")
shape (obj/get props "shape")]
[:& text-node {:index 0
:node root
:shape shape}]))
(defn- retrieve-colors
[shape]
(let [colors (into #{} (comp (map :fill)
(filter string?))
(tree-seq map? :children (:content shape)))]
(let [colors (->> shape :content
(tree-seq map? :children)
(into #{} (comp (map :fill) (filter string?))))]
(if (empty? colors)
"#000000"
(apply str (interpose "," colors)))))
(mf/defc text-shape
{::mf/wrap-props false}
[props]
{::mf/wrap-props false
::mf/forward-ref true}
[props ref]
(let [shape (unchecked-get props "shape")
selected? (unchecked-get props "selected?")
mask-id (mf/use-ctx mask-id-ctx)
{:keys [id x y width height rotation content]} shape]
grow-type (:grow-type shape)
mask-id (mf/use-ctx mask-id-ctx)
{:keys [id x y width height content]} shape]
[:foreignObject {:x x
:y y
:data-colors (retrieve-colors shape)
:transform (geom/transform-matrix shape)
:width width
:height height
:mask mask-id}
[:& text-content {:content (:content shape)}]]))
:width (if (#{:auto-width} grow-type) 10000 width)
:height (if (#{:auto-height :auto-width} grow-type) 10000 height)
:mask mask-id
:ref ref}
[:& text-content {:shape shape
:content (:content shape)}]]))

View file

@ -0,0 +1,75 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.shapes.text.embed
(:require
[clojure.set :as set]
[promesa.core :as p]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.data.fetch :as df]
[app.main.fonts :as fonts]
[app.util.text :as ut]))
(defonce font-face-template "
/* latin */
@font-face {
font-family: '$0';
font-style: $3;
font-weight: $2;
font-display: block;
src: url(/fonts/%(0)s-$1.woff) format('woff');
}
")
;; -- Embed fonts into styles
(defn get-node-fonts [node]
(let [current-font (if (not (nil? (:font-id node)))
#{(select-keys node [:font-id :font-variant-id])}
#{})
children-font (map get-node-fonts (:children node))]
(reduce set/union (conj children-font current-font))))
(defn get-local-font-css [font-id font-variant-id]
(let [{:keys [family variants]} (get @fonts/fontsdb font-id)
{:keys [name weight style]} (->> variants (filter #(= (:id %) font-variant-id)) first)
css-str (str/format font-face-template [family name weight style])]
(p/resolved css-str)))
(defn get-text-font-data [text]
(->> text
(re-seq #"url\(([^)]+)\)")
(map second)
(map df/fetch-as-data-uri)
(p/all)))
(defn embed-font [{:keys [font-id font-variant-id] :or {font-variant-id "regular"}}]
(let [{:keys [backend]} (get @fonts/fontsdb font-id)]
(p/let [font-text (case backend
:google (fonts/fetch-font font-id font-variant-id)
(get-local-font-css font-id font-variant-id))
url-to-data (get-text-font-data font-text)
replace-text (fn [text [url data]] (str/replace text url data))]
(reduce replace-text font-text url-to-data))))
(mf/defc embed-fontfaces-style [{:keys [node]}]
(let [embeded-fonts (mf/use-state nil)]
(mf/use-effect
(mf/deps node)
(fn []
(let [font-to-embed (get-node-fonts node)
font-to-embed (if (empty? font-to-embed) #{ut/default-text-attrs} font-to-embed)
embeded (map embed-font font-to-embed)]
(-> (p/all embeded)
(p/then (fn [result] (reset! embeded-fonts (str/join "\n" result))))))))
(when (not (nil? @embeded-fonts))
[:style @embeded-fonts])))

View file

@ -0,0 +1,119 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.shapes.text.styles
(:require
[cuerdas.core :as str]
[app.main.fonts :as fonts]
[app.common.data :as d]
[app.util.object :as obj]
[app.util.color :as uc]
[app.util.text :as ut]))
(defn generate-root-styles
[data props]
(let [valign (obj/get data "vertical-align" "top")
talign (obj/get data "text-align" "flex-start")
shape (obj/get props "shape")
base #js {:height (or (:height shape) "100%")
:width (or (:width shape) "100%")
:display "flex"}]
(cond-> base
(= valign "top") (obj/set! "alignItems" "flex-start")
(= valign "center") (obj/set! "alignItems" "center")
(= valign "bottom") (obj/set! "alignItems" "flex-end")
(= talign "left") (obj/set! "justifyContent" "flex-start")
(= talign "center") (obj/set! "justifyContent" "center")
(= talign "right") (obj/set! "justifyContent" "flex-end")
(= talign "justify") (obj/set! "justifyContent" "stretch"))))
(defn generate-paragraph-set-styles
[data]
;; The position absolute is used so the paragraph is "outside"
;; the normal layout and can grow outside its parent
;; We use this element to measure the size of the text
(let [base #js {:display "inline-block"
:position "absolute"}]
base))
(defn generate-paragraph-styles
[data]
(let [base #js {:fontSize "14px"
:margin "inherit"
:lineHeight "1.2"}
lh (obj/get data "line-height")
ta (obj/get data "text-align")]
(cond-> base
ta (obj/set! "textAlign" ta)
lh (obj/set! "lineHeight" lh))))
(defn generate-text-styles
[data]
(let [letter-spacing (obj/get data "letter-spacing")
text-decoration (obj/get data "text-decoration")
text-transform (obj/get data "text-transform")
line-height (obj/get data "line-height")
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
font-variant-id (obj/get data "font-variant-id")
font-family (obj/get data "font-family")
font-size (obj/get data "font-size")
;; Old properties for backwards compatibility
fill (obj/get data "fill")
opacity (obj/get data "opacity" 1)
fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity)
fill-color-gradient (obj/get data "fill-color-gradient" nil)
fill-color-gradient (when fill-color-gradient
(-> (js->clj fill-color-gradient :keywordize-keys true)
(update :type keyword)))
fill-color-ref-id (obj/get data "fill-color-ref-id")
fill-color-ref-file (obj/get data "fill-color-ref-file")
[r g b a] (uc/hex->rgba fill-color fill-opacity)
background (if fill-color-gradient
(uc/gradient->css (js->clj fill-color-gradient))
(str/format "rgba(%s, %s, %s, %s)" r g b a))
fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration
:textTransform text-transform
:lineHeight (or line-height "inherit")
"--text-color" background}]
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))
(obj/set! base "letterSpacing" (str letter-spacing "px")))
(when (and (string? font-size)
(pos? (alength font-size)))
(obj/set! base "fontSize" (str font-size "px")))
(when (and (string? font-id)
(pos? (alength font-id)))
(let [font (get fontsdb font-id)]
(let [font-family (or (:family font)
(obj/get data "fontFamily"))
font-variant (d/seek #(= font-variant-id (:id %))
(:variants font))
font-style (or (:style font-variant)
(obj/get data "fontStyle"))
font-weight (or (:weight font-variant)
(obj/get data "fontWeight"))]
(obj/set! base "fontFamily" font-family)
(obj/set! base "fontStyle" font-style)
(obj/set! base "fontWeight" font-weight))))
base))

View file

@ -192,6 +192,16 @@
(mf/use-callback
(st/emitf dv/toggle-thumbnails-panel))
on-goback
(mf/use-callback
(mf/deps project-id file-id page-id anonymous?)
(fn []
(if anonymous?
(st/emit! (rt/nav :login))
(st/emit! (rt/nav :workspace
{:project-id project-id
:file-id file-id}
{:page-id page-id})))))
on-edit
(mf/use-callback
(mf/deps project-id file-id page-id)
@ -220,7 +230,7 @@
[:header.viewer-header
[:div.main-icon
[:a {:on-click on-edit} i/logo-icon]]
[:a {:on-click on-goback} i/logo-icon]]
[:div.sitemap-zone {:alt (t locale "viewer.header.sitemap")
:on-click on-click}

View file

@ -12,7 +12,7 @@
[app.main.data.workspace.drawing :as dd]
[app.main.store :as st]
[app.main.ui.workspace.shapes :as shapes]
[app.main.ui.workspace.shapes.path :refer [path-editor]]
[app.main.ui.workspace.shapes.path.editor :refer [path-editor]]
[app.common.geom.shapes :as gsh]
[app.common.data :as d]
[app.util.dom :as dom]

View file

@ -0,0 +1,78 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.effects
(:require
[rumext.alpha :as mf]
[app.util.dom :as dom]
[app.main.data.workspace.selection :as dws]
[app.main.store :as st]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.ui.keyboard :as kbd]))
(defn use-pointer-enter
[{:keys [id]}]
(mf/use-callback
(mf/deps id)
(fn []
(st/emit! (dws/change-hover-state id true)))))
(defn use-pointer-leave
[{:keys [id]}]
(mf/use-callback
(mf/deps id)
(fn []
(st/emit! (dws/change-hover-state id false)))))
(defn use-context-menu
[shape]
(mf/use-callback
(mf/deps shape)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [position (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position position :shape shape}))))))
(defn use-mouse-down
[{:keys [id type blocked]}]
(mf/use-callback
(mf/deps id type blocked)
(fn [event]
(let [selected @refs/selected-shapes
edition @refs/selected-edition
selected? (contains? selected id)
drawing? @refs/selected-drawing-tool
button (.-which (.-nativeEvent event))]
(when-not blocked
(cond
(not= 1 button)
nil
drawing?
nil
(= type :frame)
(do (dom/stop-propagation event)
(st/emit! (dw/start-move-selected)))
:else
(do
(dom/stop-propagation event)
(if selected?
(when (kbd/shift? event)
(st/emit! (dw/select-shape id true)))
(do
(when-not (or (empty? selected) (kbd/shift? event))
(st/emit! (dw/deselect-all)))
(st/emit! (dw/select-shape id))))
(when (not= edition id)
(st/emit! (dw/start-move-selected))))))))))

View file

@ -32,7 +32,7 @@
[app.util.debug :refer [debug?]]
[app.main.ui.workspace.shapes.outline :refer [outline]]
[app.main.ui.measurements :as msr]
[app.main.ui.workspace.shapes.path :refer [path-editor]]))
[app.main.ui.workspace.shapes.path.editor :refer [path-editor]]))
(def rotation-handler-size 25)
(def resize-point-radius 4)
@ -65,52 +65,47 @@
:position :top-left
:props {:cx x :cy y}}
;; TOP
{:type :rotation
:position :top-right
:props {:cx (+ x width) :cy y}}
{:type :resize-point
:position :top-right
:props {:cx (+ x width) :cy y}}
{:type :rotation
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
{:type :resize-point
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
{:type :rotation
:position :bottom-left
:props {:cx x :cy (+ y height)}}
{:type :resize-point
:position :bottom-left
:props {:cx x :cy (+ y height)}}
{:type :resize-side
:position :top
:props {:x x :y y :length width :angle 0 }}
;; TOP-RIGHT
{:type :rotation
:position :top-right
:props {:cx (+ x width) :cy y}}
{:type :resize-point
:position :top-right
:props {:cx (+ x width) :cy y}}
;; RIGHT
{:type :resize-side
:position :right
:props {:x (+ x width) :y y :length height :angle 90 }}
;; BOTTOM-RIGHT
{:type :rotation
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
{:type :resize-point
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
;; BOTTOM
{:type :resize-side
:position :bottom
:props {:x (+ x width) :y (+ y height) :length width :angle 180 }}
;; BOTTOM-LEFT
{:type :rotation
:position :bottom-left
:props {:cx x :cy (+ y height)}}
{:type :resize-point
:position :bottom-left
:props {:cx x :cy (+ y height)}}
;; LEFT
{:type :resize-side
:position :left
:props {:x x :y (+ y height) :length height :angle 270 }}])
:props {:x x :y (+ y height) :length height :angle 270 }}
])
(mf/defc rotation-handler [{:keys [cx cy transform position rotation zoom on-rotate]}]
(let [size (/ rotation-handler-size zoom)
@ -160,11 +155,13 @@
(mf/defc resize-side-handler [{:keys [x y length angle zoom position rotation transform on-resize]}]
(let [res-point (if (#{:top :bottom} position)
{:y y}
{:x x})]
[:rect {:x (+ x (/ resize-point-rect-size zoom))
{:x x})
width length #_(max 0 (- length (/ (* resize-point-rect-size 2) zoom)))
height (/ resize-side-height zoom)]
[:rect {:x x
:y (- y (/ resize-side-height 2 zoom))
:width (max 0 (- length (/ (* resize-point-rect-size 2) zoom)))
:height (/ resize-side-height zoom)
:width width
:height height
:transform (gmt/multiply transform
(gmt/rotate-matrix angle (gpt/point x y)))
:on-mouse-down #(on-resize res-point %)
@ -183,7 +180,7 @@
current-transform (mf/deref refs/current-transform)
selrect (:selrect shape)
transform (geom/transform-matrix shape)
transform (geom/transform-matrix shape {:no-flip true})
tr-shape (geom/transform-shape shape)]

View file

@ -19,7 +19,6 @@
[app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.image :as image]
[app.main.data.workspace.selection :as dws]
[app.main.store :as st]
[app.main.refs :as refs]
@ -54,20 +53,6 @@
(and (identical? n-shape o-shape)
(identical? n-frame o-frame)))))
(defn use-mouse-enter
[{:keys [id] :as shape}]
(mf/use-callback
(mf/deps id)
(fn []
(st/emit! (dws/change-hover-state id true)))))
(defn use-mouse-leave
[{:keys [id] :as shape}]
(mf/use-callback
(mf/deps id)
(fn []
(st/emit! (dws/change-hover-state id false)))))
(defn make-is-moving-ref
[id]
(let [check-moving (fn [local]
@ -86,8 +71,6 @@
(geom/translate-to-frame frame))
opts #js {:shape shape
:frame frame}
on-mouse-enter (use-mouse-enter shape)
on-mouse-leave (use-mouse-leave shape)
alt? (hooks/use-rxsub ms/keyboard-alt)
@ -95,15 +78,10 @@
#(make-is-moving-ref (:id shape)))
moving? (mf/deref moving-iref)]
(mf/use-effect
(constantly on-mouse-leave))
(when (and shape
(or ghost? (not moving?))
(not (:hidden shape)))
[:g.shape-wrapper {:on-mouse-enter on-mouse-enter
:on-mouse-leave on-mouse-leave
:style {:cursor (if alt? cur/duplicate nil)}}
[:g.shape-wrapper {:style {:cursor (if alt? cur/duplicate nil)}}
(case (:type shape)
:path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts]

View file

@ -9,73 +9,19 @@
(ns app.main.ui.workspace.shapes.common
(:require
[rumext.alpha :as mf]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.keyboard :as kbd]
[app.util.dom :as dom]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.main.ui.shapes.shape :refer [shape-container]]))
(defn- on-mouse-down
[event {:keys [id type] :as shape}]
(let [selected @refs/selected-shapes
edition @refs/selected-edition
selected? (contains? selected id)
drawing? @refs/selected-drawing-tool
button (.-which (.-nativeEvent event))]
(when-not (:blocked shape)
(cond
(not= 1 button)
nil
drawing?
nil
(= type :frame)
(do (dom/stop-propagation event)
(st/emit! (dw/start-move-selected)))
:else
(do
(dom/stop-propagation event)
(if selected?
(when (kbd/shift? event)
(st/emit! (dw/select-shape id true)))
(do
(when-not (or (empty? selected) (kbd/shift? event))
(st/emit! (dw/deselect-all)))
(st/emit! (dw/select-shape id))))
(when (not= edition id)
(st/emit! (dw/start-move-selected))))))))
(defn on-context-menu
[event shape]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [position (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position position :shape shape}))))
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.effects :as we]
[rumext.alpha :as mf]))
(defn generic-wrapper-factory
[component]
(mf/fnc generic-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
on-mouse-down (mf/use-callback
(mf/deps shape)
#(on-mouse-down % shape))
on-context-menu (mf/use-callback
(mf/deps shape)
#(on-context-menu % shape))]
(let [shape (unchecked-get props "shape")]
[:> shape-container {:shape shape
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
:on-mouse-down (we/use-mouse-down shape)
:on-context-menu (we/use-context-menu shape)
:on-pointer-enter (we/use-pointer-enter shape)
:on-pointer-leave (we/use-pointer-leave shape)}
[:& component {:shape shape}]])))

View file

@ -9,23 +9,18 @@
(ns app.main.ui.workspace.shapes.frame
(:require
[okulary.core :as l]
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.constants :as c]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.workspace.shapes.common :as common]
[app.main.data.workspace.selection :as dws]
[app.main.ui.shapes.frame :as frame]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.effects :as we]
[app.util.dom :as dom]
[app.main.streams :as ms]
[app.util.timers :as ts]
[app.main.ui.shapes.shape :refer [shape-container]]))
[okulary.core :as l]
[rumext.alpha :as mf]))
(defn- frame-wrapper-factory-equals?
[np op]
@ -45,29 +40,41 @@
(recur (first ids) (rest ids))
false))))))
(defn use-select-shape [{:keys [id]}]
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/prevent-default event)
(st/emit! (dw/deselect-all)
(dw/select-shape id)))))
;; Ensure that the label has always the same font
;; size, regardless of zoom
;; https://css-tricks.com/transforms-on-svg-elements/
(defn text-transform
[{:keys [x y]} zoom]
(let [inv-zoom (/ 1 zoom)]
(str
"scale(" inv-zoom ", " inv-zoom ") "
"translate(" (* zoom x) ", " (* zoom y) ")")))
(mf/defc frame-title
[{:keys [frame on-double-click on-mouse-over on-mouse-out]}]
[{:keys [frame]}]
(let [zoom (mf/deref refs/selected-zoom)
inv-zoom (/ 1 zoom)
{:keys [width x y]} frame
label-pos (gpt/point x (- y (/ 10 zoom)))]
label-pos (gpt/point x (- y (/ 10 zoom)))
handle-click (use-select-shape frame)
handle-pointer-enter (we/use-pointer-enter frame)
handle-pointer-leave (we/use-pointer-leave frame)]
[:text {:x 0
:y 0
:width width
:height 20
:class "workspace-frame-label"
;; Ensure that the label has always the same font
;; size, regardless of zoom
;; https://css-tricks.com/transforms-on-svg-elements/
:transform (str
"scale(" inv-zoom ", " inv-zoom ") "
"translate(" (* zoom (:x label-pos)) ", "
(* zoom (:y label-pos))
")")
;; User may also select the frame with single click in the label
:on-click on-double-click
:on-mouse-over on-mouse-over
:on-mouse-out on-mouse-out}
:transform (text-transform label-pos zoom)
:on-click handle-click
:on-pointer-enter handle-pointer-enter
:on-pointer-leave handle-pointer-leave}
(:name frame)]))
(defn make-is-moving-ref
@ -97,47 +104,23 @@
#(refs/make-selected-ref (:id shape)))
selected? (mf/deref selected-iref)
on-mouse-down (mf/use-callback (mf/deps shape)
#(common/on-mouse-down % shape))
on-context-menu (mf/use-callback (mf/deps shape)
#(common/on-context-menu % shape))
shape (geom/transform-shape shape)
shape (gsh/transform-shape shape)
children (mapv #(get objects %) (:shapes shape))
ds-modifier (get-in shape [:modifiers :displacement])
on-double-click
(mf/use-callback
(mf/deps (:id shape))
(fn [event]
(dom/prevent-default event)
(st/emit! (dw/deselect-all)
(dw/select-shape (:id shape)))))
on-mouse-over
(mf/use-callback
(mf/deps (:id shape))
(fn []
(st/emit! (dws/change-hover-state (:id shape) true))))
on-mouse-out
(mf/use-callback
(mf/deps (:id shape))
(fn []
(st/emit! (dws/change-hover-state (:id shape) false))))]
handle-context-menu (we/use-context-menu shape)
handle-double-click (use-select-shape shape)
handle-mouse-down (we/use-mouse-down shape)]
(when (and shape
(or ghost? (not moving?))
(not (:hidden shape)))
[:g {:class (when selected? "selected")
:on-context-menu on-context-menu
;; :on-double-click on-double-click
:on-mouse-down on-mouse-down}
:on-context-menu handle-context-menu
:on-double-click handle-double-click
:on-mouse-down handle-mouse-down}
[:& frame-title {:frame shape
:on-context-menu on-context-menu
:on-double-click on-double-click
:on-mouse-down on-mouse-down}]
[:& frame-title {:frame shape}]
[:> shape-container {:shape shape}
[:& frame-shape

View file

@ -9,18 +9,15 @@
(ns app.main.ui.workspace.shapes.group
(:require
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.constants :as c]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.workspace.shapes.common :as common]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.group :as group]
[app.util.dom :as dom]
[app.main.streams :as ms]
[app.util.timers :as ts]))
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.effects :as we]
[app.util.dom :as dom]
[rumext.alpha :as mf]))
(defn- group-wrapper-factory-equals?
[np op]
@ -31,6 +28,14 @@
(and (= n-frame o-frame)
(= n-shape o-shape))))
(defn use-double-click [{:keys [id]}]
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/select-inside-group id @ms/mouse-position)))))
(defn group-wrapper-factory
[shape-wrapper]
(let [group-shape (group/group-shape shape-wrapper)]
@ -41,14 +46,8 @@
(let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame")
on-mouse-down
(mf/use-callback (mf/deps shape) #(common/on-mouse-down % shape))
on-context-menu
(mf/use-callback (mf/deps shape) #(common/on-context-menu % shape))
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
childs (mf/deref childs-ref)
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
childs (mf/deref childs-ref)
is-child-selected-ref
(mf/use-memo (mf/deps (:id shape)) #(refs/is-child-selected? (:id shape)))
@ -59,24 +58,23 @@
mask-id (when (:masked-group? shape) (first (:shapes shape)))
is-mask-selected-ref
(mf/use-memo (mf/deps mask-id)
#(refs/make-selected-ref mask-id))
(mf/use-memo (mf/deps mask-id) #(refs/make-selected-ref mask-id))
is-mask-selected?
(mf/deref is-mask-selected-ref)
on-double-click
(mf/use-callback
(mf/deps (:id shape))
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/select-inside-group (:id shape) @ms/mouse-position))))]
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
handle-double-click (use-double-click shape)]
[:> shape-container {:shape shape
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu
:on-double-click on-double-click}
:on-mouse-down handle-mouse-down
:on-context-menu handle-context-menu
:on-pointer-enter handle-pointer-enter
:on-pointer-leave handle-pointer-leave
:on-double-click handle-double-click}
[:& group-shape
{:frame frame
:shape shape

View file

@ -9,330 +9,48 @@
(ns app.main.ui.workspace.shapes.path
(:require
[rumext.alpha :as mf]
[goog.events :as events]
[okulary.core :as l]
[app.util.data :as d]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.main.refs :as refs]
[app.main.streams :as ms]
[app.main.constants :as c]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.data.workspace :as dw]
[app.main.data.workspace.drawing :as dr]
[app.main.data.workspace.drawing.path :as drp]
[app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.shapes.common :as common]
[app.main.ui.workspace.effects :as we]
[app.main.ui.workspace.shapes.path.common :as pc]
[app.util.dom :as dom]
[app.util.geom.path :as ugp]
[app.common.geom.point :as gpt]
[app.main.ui.cursors :as cur]
[app.main.ui.icons :as i])
(:import goog.events.EventType))
[rumext.alpha :as mf]))
(def primary-color "#1FDEA7")
(def secondary-color "#DB00FF")
(def black-color "#000000")
(def white-color "#FFFFFF")
(def gray-color "#B1B2B5")
(def current-edit-path-ref
(let [selfn (fn [local]
(let [id (:edition local)]
(get-in local [:edit-path id])))]
(l/derived selfn refs/workspace-local)))
(defn make-edit-path-ref [id]
(mf/use-memo
(defn use-double-click [{:keys [id]}]
(mf/use-callback
(mf/deps id)
(let [selfn #(get-in % [:edit-path id])]
#(l/derived selfn refs/workspace-local))))
(defn make-content-modifiers-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id :content-modifiers])]
#(l/derived selfn refs/workspace-local))))
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/start-edition-mode id)
(dw/start-path-edit id)))))
(mf/defc path-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
hover? (or (mf/deref refs/current-hover) #{})
on-mouse-down (mf/use-callback
(mf/deps shape)
#(common/on-mouse-down % shape))
on-context-menu (mf/use-callback
(mf/deps shape)
#(common/on-context-menu % shape))
on-double-click (mf/use-callback
(mf/deps shape)
(fn [event]
(when (not (::dr/initialized? shape))
(do
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dw/start-edition-mode (:id shape))
(dw/start-path-edit (:id shape)))))))
content-modifiers-ref (make-content-modifiers-ref (:id shape))
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
content-modifiers (mf/deref content-modifiers-ref)
editing-id (mf/deref refs/selected-edition)
editing? (= editing-id (:id shape))
shape (update shape :content ugp/apply-content-modifiers content-modifiers)]
shape (update shape :content ugp/apply-content-modifiers content-modifiers)
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
handle-double-click (use-double-click shape)]
[:> shape-container {:shape shape
:pointer-events (when editing? "none")
:on-double-click on-double-click
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
:on-mouse-down handle-mouse-down
:on-context-menu handle-context-menu
:on-pointer-enter handle-pointer-enter
:on-pointer-leave handle-pointer-leave
:on-double-click handle-double-click}
[:& path/path-shape {:shape shape
:background? true}]]))
(mf/defc path-actions [{:keys [shape]}]
(let [id (mf/deref refs/selected-edition)
{:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref current-edit-path-ref)]
[:div.path-actions
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen]
[:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-corner)))} i/nodes-corner]
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-curve)))} i/nodes-curve]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]]))
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}]
(let [{:keys [x y]} position
on-enter
(fn [event]
(st/emit! (drp/path-pointer-enter position)))
on-leave
(fn [event]
(st/emit! (drp/path-pointer-leave position)))
on-click
(fn [event]
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(and (= edit-mode :move) (not selected?))
(st/emit! (drp/select-node position))
(and (= edit-mode :move) selected?)
(st/emit! (drp/deselect-node position))))))
on-mouse-down
(fn [event]
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-path-point position))
(and (= edit-mode :draw) start-path?)
(st/emit! (drp/start-path-from-point position))
(and (= edit-mode :draw) (not start-path?))
(st/emit! (drp/close-path-drag-start position))))))]
[:g.path-point
[:circle.path-point
{:cx x
:cy y
:r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom))
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
preview? secondary-color
:else primary-color)
:fill (cond selected? primary-color
:else white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (cond
(and (not last-p?) (= edit-mode :draw)) cur/pen-node
(= edit-mode :move) cur/pointer-node)
:fill "transparent"}}]]))
(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}]
(when (and point handler)
(let [{:keys [x y]} handler
on-enter
(fn [event]
(st/emit! (drp/path-handler-enter index prefix)))
on-leave
(fn [event]
(st/emit! (drp/path-handler-leave index prefix)))
on-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(drp/select-handler index prefix)))
on-mouse-down
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-handler index prefix))))]
[:g.handler {:pointer-events (when (= edit-mode :draw))}
[:line
{:x1 (:x point)
:y1 (:y point)
:x2 x
:y2 y
:style {:stroke (if hover? black-color gray-color)
:stroke-width (/ 1 zoom)}}]
[:rect
{:x (- x (/ 3 zoom))
:y (- y (/ 3 zoom))
:width (/ 6 zoom)
:height (/ 6 zoom)
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) black-color
:else primary-color)
:fill (cond selected? primary-color
:else white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (when (= edit-mode :move) cur/pointer-move)
:fill "transparent"}}]])))
(mf/defc path-preview [{:keys [zoom command from]}]
[:g.preview {:style {:pointer-events "none"}}
(when (not= :move-to (:command command))
[:path {:style {:fill "transparent"
:stroke secondary-color
:stroke-width (/ 1 zoom)}
:d (ugp/content->path [{:command :move-to
:params {:x (:x from)
:y (:y from)}}
command])}])
[:& path-point {:position (:params command)
:preview? true
:zoom zoom}]])
(mf/defc path-editor
[{:keys [shape zoom]}]
(let [editor-ref (mf/use-ref nil)
edit-path-ref (make-edit-path-ref (:id shape))
{:keys [edit-mode
drag-handler
prev-handler
preview
content-modifiers
last-point
selected-handlers
selected-points
hover-handlers
hover-points]} (mf/deref edit-path-ref)
{:keys [content]} shape
content (ugp/apply-content-modifiers content content-modifiers)
points (->> content ugp/content->points (into #{}))
last-command (last content)
last-p (->> content last ugp/command->point)
handlers (ugp/content->handlers content)
handle-click-outside
(fn [event]
(let [current (dom/get-target event)
editor-dom (mf/ref-val editor-ref)]
(when-not (or (.contains editor-dom current)
(dom/class? current "viewport-actions-entry"))
(st/emit! (drp/deselect-all)))))]
(mf/use-layout-effect
(fn []
(let [keys [(events/listen (dom/get-root) EventType.CLICK handle-click-outside)]]
#(doseq [key keys]
(events/unlistenByKey key)))))
[:g.path-editor {:ref editor-ref}
(when (and preview (not drag-handler))
[:& path-preview {:command preview
:from last-p
:zoom zoom}])
(for [position points]
[:g.path-node
[:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")}
(for [[index prefix] (get handlers position)]
(let [command (get content index)
x (get-in command [:params (d/prefix-keyword prefix :x)])
y (get-in command [:params (d/prefix-keyword prefix :y)])
handler-position (gpt/point x y)]
(when (not= position handler-position)
[:& path-handler {:point position
:handler handler-position
:index index
:prefix prefix
:zoom zoom
:selected? (contains? selected-handlers [index prefix])
:hover? (contains? hover-handlers [index prefix])
:edit-mode edit-mode}])))]
[:& path-point {:position position
:zoom zoom
:edit-mode edit-mode
:selected? (contains? selected-points position)
:hover? (contains? hover-points position)
:last-p? (= last-point position)
:start-path? (nil? last-point)}]])
(when prev-handler
[:g.prev-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler prev-handler
:zoom zoom}]])
(when drag-handler
[:g.drag-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler drag-handler
:zoom zoom}]])]))

View file

@ -0,0 +1,47 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.path.actions
(:require
[app.main.data.workspace.drawing.path :as drp]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.workspace.shapes.path.common :as pc]
[rumext.alpha :as mf]))
(mf/defc path-actions [{:keys [shape]}]
(let [id (mf/deref refs/selected-edition)
{:keys [edit-mode selected-points snap-toggled] :as all} (mf/deref pc/current-edit-path-ref)]
[:div.path-actions
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (= edit-mode :draw) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :draw))} i/pen]
[:div.viewport-actions-entry {:class (when (= edit-mode :move) "is-toggled")
:on-click #(st/emit! (drp/change-edit-mode :move))} i/pointer-inner]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-add]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-remove]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-merge]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-join]
[:div.viewport-actions-entry {:class "is-disabled"} i/nodes-separate]]
[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-corner)))} i/nodes-corner]
[:div.viewport-actions-entry {:class (when (empty? selected-points) "is-disabled")
:on-click #(when-not (empty? selected-points)
(st/emit! (drp/make-curve)))} i/nodes-curve]]
#_[:div.viewport-actions-group
[:div.viewport-actions-entry {:class (when snap-toggled "is-toggled")} i/nodes-snap]]]))

View file

@ -0,0 +1,39 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.path.common
(:require
[app.main.refs :as refs]
[okulary.core :as l]
[rumext.alpha :as mf]))
(def primary-color "#1FDEA7")
(def secondary-color "#DB00FF")
(def black-color "#000000")
(def white-color "#FFFFFF")
(def gray-color "#B1B2B5")
(def current-edit-path-ref
(let [selfn (fn [local]
(let [id (:edition local)]
(get-in local [:edit-path id])))]
(l/derived selfn refs/workspace-local)))
(defn make-edit-path-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id])]
#(l/derived selfn refs/workspace-local))))
(defn make-content-modifiers-ref [id]
(mf/use-memo
(mf/deps id)
(let [selfn #(get-in % [:edit-path id :content-modifiers])]
#(l/derived selfn refs/workspace-local))))

View file

@ -0,0 +1,235 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.path.editor
(:require
[app.common.geom.point :as gpt]
[app.main.data.workspace.drawing.path :as drp]
[app.main.store :as st]
[app.main.ui.cursors :as cur]
[app.main.ui.workspace.shapes.path.common :as pc]
[app.util.data :as d]
[app.util.dom :as dom]
[app.util.geom.path :as ugp]
[goog.events :as events]
[rumext.alpha :as mf])
(:import goog.events.EventType))
(mf/defc path-point [{:keys [position zoom edit-mode hover? selected? preview? start-path? last-p?]}]
(let [{:keys [x y]} position
on-enter
(fn [event]
(st/emit! (drp/path-pointer-enter position)))
on-leave
(fn [event]
(st/emit! (drp/path-pointer-leave position)))
on-click
(fn [event]
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(and (= edit-mode :move) (not selected?))
(st/emit! (drp/select-node position))
(and (= edit-mode :move) selected?)
(st/emit! (drp/deselect-node position))))))
on-mouse-down
(fn [event]
(when-not last-p?
(do (dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-path-point position))
(and (= edit-mode :draw) start-path?)
(st/emit! (drp/start-path-from-point position))
(and (= edit-mode :draw) (not start-path?))
(st/emit! (drp/close-path-drag-start position))))))]
[:g.path-point
[:circle.path-point
{:cx x
:cy y
:r (if (or selected? hover?) (/ 3.5 zoom) (/ 3 zoom))
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) pc/black-color
preview? pc/secondary-color
:else pc/primary-color)
:fill (cond selected? pc/primary-color
:else pc/white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (cond
(and (not last-p?) (= edit-mode :draw)) cur/pen-node
(= edit-mode :move) cur/pointer-node)
:fill "transparent"}}]]))
(mf/defc path-handler [{:keys [index prefix point handler zoom selected? hover? edit-mode]}]
(when (and point handler)
(let [{:keys [x y]} handler
on-enter
(fn [event]
(st/emit! (drp/path-handler-enter index prefix)))
on-leave
(fn [event]
(st/emit! (drp/path-handler-leave index prefix)))
on-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(drp/select-handler index prefix)))
on-mouse-down
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(cond
(= edit-mode :move)
(st/emit! (drp/start-move-handler index prefix))))]
[:g.handler {:pointer-events (when (= edit-mode :draw))}
[:line
{:x1 (:x point)
:y1 (:y point)
:x2 x
:y2 y
:style {:stroke (if hover? pc/black-color pc/gray-color)
:stroke-width (/ 1 zoom)}}]
[:rect
{:x (- x (/ 3 zoom))
:y (- y (/ 3 zoom))
:width (/ 6 zoom)
:height (/ 6 zoom)
:style {:stroke-width (/ 1 zoom)
:stroke (cond (or selected? hover?) pc/black-color
:else pc/primary-color)
:fill (cond selected? pc/primary-color
:else pc/white-color)}}]
[:circle {:cx x
:cy y
:r (/ 10 zoom)
:on-click on-click
:on-mouse-down on-mouse-down
:on-mouse-enter on-enter
:on-mouse-leave on-leave
:style {:cursor (when (= edit-mode :move) cur/pointer-move)
:fill "transparent"}}]])))
(mf/defc path-preview [{:keys [zoom command from]}]
[:g.preview {:style {:pointer-events "none"}}
(when (not= :move-to (:command command))
[:path {:style {:fill "transparent"
:stroke pc/secondary-color
:stroke-width (/ 1 zoom)}
:d (ugp/content->path [{:command :move-to
:params {:x (:x from)
:y (:y from)}}
command])}])
[:& path-point {:position (:params command)
:preview? true
:zoom zoom}]])
(mf/defc path-editor
[{:keys [shape zoom]}]
(let [editor-ref (mf/use-ref nil)
edit-path-ref (pc/make-edit-path-ref (:id shape))
{:keys [edit-mode
drag-handler
prev-handler
preview
content-modifiers
last-point
selected-handlers
selected-points
hover-handlers
hover-points]} (mf/deref edit-path-ref)
{:keys [content]} shape
content (ugp/apply-content-modifiers content content-modifiers)
points (->> content ugp/content->points (into #{}))
last-command (last content)
last-p (->> content last ugp/command->point)
handlers (ugp/content->handlers content)
handle-click-outside
(fn [event]
(let [current (dom/get-target event)
editor-dom (mf/ref-val editor-ref)]
(when-not (or (.contains editor-dom current)
(dom/class? current "viewport-actions-entry"))
(st/emit! (drp/deselect-all)))))]
(mf/use-layout-effect
(fn []
(let [keys [(events/listen (dom/get-root) EventType.CLICK handle-click-outside)]]
#(doseq [key keys]
(events/unlistenByKey key)))))
[:g.path-editor {:ref editor-ref}
(when (and preview (not drag-handler))
[:& path-preview {:command preview
:from last-p
:zoom zoom}])
(for [position points]
[:g.path-node
[:g.point-handlers {:pointer-events (when (= edit-mode :draw) "none")}
(for [[index prefix] (get handlers position)]
(let [command (get content index)
x (get-in command [:params (d/prefix-keyword prefix :x)])
y (get-in command [:params (d/prefix-keyword prefix :y)])
handler-position (gpt/point x y)]
(when (not= position handler-position)
[:& path-handler {:point position
:handler handler-position
:index index
:prefix prefix
:zoom zoom
:selected? (contains? selected-handlers [index prefix])
:hover? (contains? hover-handlers [index prefix])
:edit-mode edit-mode}])))]
[:& path-point {:position position
:zoom zoom
:edit-mode edit-mode
:selected? (contains? selected-points position)
:hover? (contains? hover-points position)
:last-p? (= last-point position)
:start-path? (nil? last-point)}]])
(when prev-handler
[:g.prev-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler prev-handler
:zoom zoom}]])
(when drag-handler
[:g.drag-handler {:pointer-events "none"}
[:& path-handler {:point last-p
:handler drag-handler
:zoom zoom}]])]))

View file

@ -9,455 +9,128 @@
(ns app.main.ui.workspace.shapes.text
(:require
["slate" :as slate]
["slate-react" :as rslate]
[app.common.data :as d]
[app.common.geom.shapes :as geom]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.texts :as dwt]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as muc]
[app.main.ui.cursors :as cur]
[app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.shapes.text :as text]
[app.main.ui.workspace.effects :as we]
[app.main.ui.workspace.shapes.common :as common]
[app.util.color :as color]
[app.util.color :as uc]
[app.main.ui.workspace.shapes.text.editor :as editor]
[app.util.dom :as dom]
[app.util.object :as obj]
[app.util.text :as ut]
[app.util.timers :as timers]
[beicon.core :as rx]
[cuerdas.core :as str]
[goog.events :as events]
[goog.object :as gobj]
[rumext.alpha :as mf])
(:import
goog.events.EventType
goog.events.KeyCodes))
[rumext.alpha :as mf]))
;; --- Events
(defn handle-mouse-down
[event {:keys [id group] :as shape}]
(if (and (not (:blocked shape))
(or @refs/selected-drawing-tool
@refs/selected-edition))
(dom/stop-propagation event)
(common/on-mouse-down event shape)))
(defn use-double-click [{:keys [id]} selected?]
(mf/use-callback
(mf/deps id selected?)
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when selected?
(st/emit! (dw/start-edition-mode id))))))
;; --- Text Wrapper for workspace
(declare text-shape-edit)
(declare text-shape)
(defn handle-shape-resize [{:keys [id selrect grow-type overflow-text]} new-width new-height]
(let [{shape-width :width shape-height :height} selrect
undo-transaction (get-in @st/state [:workspace-undo :transaction])]
(when (not undo-transaction) (st/emit! dwc/start-undo-transaction))
(when (and (> new-width 0) (> new-height 0))
(cond
(and overflow-text (not= :fixed grow-type))
(st/emit! (dwt/update-overflow-text id false))
(and (= :fixed grow-type) (not overflow-text) (> new-height shape-height))
(st/emit! (dwt/update-overflow-text id true))
(and (= :fixed grow-type) overflow-text (<= new-height shape-height))
(st/emit! (dwt/update-overflow-text id false))
(and (or (not= shape-width new-width)
(not= shape-height new-height))
(= grow-type :auto-width))
(st/emit! (dw/update-dimensions [id] :width new-width)
(dw/update-dimensions [id] :height new-height))
(and (not= shape-height new-height) (= grow-type :auto-height))
(st/emit! (dw/update-dimensions [id] :height new-height))))
(when (not undo-transaction) (st/emit! dwc/discard-undo-transaction))))
(defn resize-observer [{:keys [id selrect grow-type overflow-text] :as shape} root query]
(mf/use-effect
(mf/deps id selrect grow-type overflow-text root query)
(fn []
(let [on-change (fn [entries]
(when (seq entries)
;; RequestAnimationFrame so the "loop limit error" error is not thrown
;; https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
(timers/raf
#(let [width (obj/get-in entries [0 "contentRect" "width"])
height (obj/get-in entries [0 "contentRect" "height"])]
(handle-shape-resize shape (mth/ceil width) (mth/ceil height))))))
observer (js/ResizeObserver. on-change)
node (when root (dom/query root query))]
(when node (.observe observer node))
#(.disconnect observer)))))
(mf/defc text-wrapper
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
selected-iref (mf/use-memo (mf/deps (:id shape))
#(refs/make-selected-ref (:id shape)))
selected? (mf/deref selected-iref)
edition (mf/deref refs/selected-edition)
(let [{:keys [id x y width height] :as shape} (unchecked-get props "shape")
selected-iref (mf/use-memo (mf/deps (:id shape))
#(refs/make-selected-ref (:id shape)))
selected? (mf/deref selected-iref)
edition (mf/deref refs/selected-edition)
current-transform (mf/deref refs/current-transform)
render-editor (mf/use-state false)
edition? (= edition (:id shape))
render-editor (mf/use-state false)
embed-resources? (mf/use-ctx muc/embed-ctx)
edition? (= edition id)
on-mouse-down #(handle-mouse-down % shape)
on-context-menu #(common/on-context-menu % shape)
embed-resources? (mf/use-ctx muc/embed-ctx)
on-double-click
(fn [event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when selected?
(st/emit! (dw/start-edition-mode (:id shape)))))
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
handle-double-click (use-double-click shape selected?)
check?
(and (#{:auto-width :auto-height} (:grow-type shape))
selected?
(not edition?)
(not embed-resources?)
(nil? current-transform))]
text-ref (mf/use-ref nil)
text-node (mf/ref-val text-ref)]
(mf/use-effect
(mf/deps check?)
(fn []
(let [sem (timers/schedule #(reset! render-editor check?))]
#(rx/dispose! sem))))
(resize-observer shape text-node ".paragraph-set")
[:> shape-container {:shape shape
:on-double-click on-double-click
:on-mouse-down on-mouse-down
:on-context-menu on-context-menu}
(when @render-editor
[:g {:opacity 0
:style {:pointer-events "none"}}
;; We only render the component for its side-effect
[:& text-shape-edit {:shape shape
:read-only? true}]])
[:> shape-container {:shape shape}
[:& text/text-shape {:key "text-shape"
:ref text-ref
:shape shape
:selected? selected?
:style {:display (when edition? "none")}}]
(when edition?
[:& editor/text-shape-edit {:key "editor"
:shape shape}])
(if edition?
[:& text-shape-edit {:shape shape}]
[:& text/text-shape {:shape shape
:selected? selected?}])]))
(when-not edition?
[:rect.text-actions
{:x x
:y y
:width width
:height height
:style {:fill "transparent"}
:on-mouse-down handle-mouse-down
:on-context-menu handle-context-menu
:on-pointer-enter handle-pointer-enter
:on-pointer-leave handle-pointer-leave
:on-double-click handle-double-click
:transform (gsh/transform-matrix shape)}])]))
;; --- Text Editor Rendering
(defn- generate-root-styles
[data props]
(let [valign (obj/get data "vertical-align" "top")
talign (obj/get data "text-align")
shape (obj/get props "shape")
base #js {:height "100%"
:width (:width shape)
:display "flex"}]
(cond-> base
(= valign "top") (obj/set! "alignItems" "flex-start")
(= valign "center") (obj/set! "alignItems" "center")
(= valign "bottom") (obj/set! "alignItems" "flex-end")
(= talign "left") (obj/set! "justifyContent" "flex-start")
(= talign "center") (obj/set! "justifyContent" "center")
(= talign "right") (obj/set! "justifyContent" "flex-end")
(= talign "justify") (obj/set! "justifyContent" "stretch"))))
(defn- generate-paragraph-styles
[data]
(let [base #js {:fontSize "14px"
:margin "inherit"
:lineHeight "1.2"}
lh (obj/get data "line-height")
ta (obj/get data "text-align")]
(cond-> base
ta (obj/set! "textAlign" ta)
lh (obj/set! "lineHeight" lh))))
(defn- generate-text-styles
[data]
(let [letter-spacing (obj/get data "letter-spacing")
text-decoration (obj/get data "text-decoration")
text-transform (obj/get data "text-transform")
line-height (obj/get data "line-height")
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
font-variant-id (obj/get data "font-variant-id")
font-family (obj/get data "font-family")
font-size (obj/get data "font-size")
;; Old properties for backwards compatibility
fill (obj/get data "fill")
opacity (obj/get data "opacity" 1)
fill-color (obj/get data "fill-color" fill)
fill-opacity (obj/get data "fill-opacity" opacity)
fill-color-gradient (obj/get data "fill-color-gradient" nil)
fill-color-gradient (when fill-color-gradient
(-> (js->clj fill-color-gradient :keywordize-keys true)
(update :type keyword)))
fill-color-ref-id (obj/get data "fill-color-ref-id")
fill-color-ref-file (obj/get data "fill-color-ref-file")
[r g b a] (uc/hex->rgba fill-color fill-opacity)
background (if fill-color-gradient
(uc/gradient->css (js->clj fill-color-gradient))
(str/format "rgba(%s, %s, %s, %s)" r g b a))
fontsdb (deref fonts/fontsdb)
base #js {:textDecoration text-decoration
:textTransform text-transform
:lineHeight (or line-height "inherit")
"--text-color" background}]
(when (and (string? letter-spacing)
(pos? (alength letter-spacing)))
(obj/set! base "letterSpacing" (str letter-spacing "px")))
(when (and (string? font-size)
(pos? (alength font-size)))
(obj/set! base "fontSize" (str font-size "px")))
(when (and (string? font-id)
(pos? (alength font-id)))
(let [font (get fontsdb font-id)]
(let [font-family (or (:family font)
(obj/get data "fontFamily"))
font-variant (d/seek #(= font-variant-id (:id %))
(:variants font))
font-style (or (:style font-variant)
(obj/get data "fontStyle"))
font-weight (or (:weight font-variant)
(obj/get data "fontWeight"))]
(obj/set! base "fontFamily" font-family)
(obj/set! base "fontStyle" font-style)
(obj/set! base "fontWeight" font-weight))))
base))
(mf/defc editor-root-node
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [attrs (obj/get props "attributes")
childs (obj/get props "children")
data (obj/get props "element")
type (obj/get data "type")
style (generate-root-styles data props)
attrs (obj/set! attrs "style" style)
attrs (obj/set! attrs "className" type)]
[:> :div attrs childs]))
(mf/defc editor-paragraph-set-node
{::mf/wrap-props false}
[props]
(let [attrs (obj/get props "attributes")
childs (obj/get props "children")
data (obj/get props "element")
type (obj/get data "type")
shape (obj/get props "shape")
;; The position absolute is used so the paragraph is "outside"
;; the normal layout and can grow outside its parent
;; We use this element to measure the size of the text
style #js {:display "inline-block"
:position "absolute"}
attrs (obj/set! attrs "style" style)
attrs (obj/set! attrs "className" type)]
[:> :div attrs childs]))
(mf/defc editor-paragraph-node
{::mf/wrap-props false}
[props]
(let [attrs (obj/get props "attributes")
childs (obj/get props "children")
data (obj/get props "element")
style (generate-paragraph-styles data)
attrs (obj/set! attrs "style" style)]
[:> :p attrs childs]))
(mf/defc editor-text-node
{::mf/wrap-props false}
[props]
(let [attrs (obj/get props "attributes")
childs (obj/get props "children")
data (obj/get props "leaf")
style (generate-text-styles data)
attrs (-> attrs
(obj/set! "style" style)
(obj/set! "className" "text-node"))]
[:> :span attrs childs]))
(defn- render-element
[shape props]
(mf/html
(let [element (obj/get props "element")
props (obj/merge! props #js {:shape shape})]
(case (obj/get element "type")
"root" [:> editor-root-node props]
"paragraph-set" [:> editor-paragraph-set-node props]
"paragraph" [:> editor-paragraph-node props]
nil))))
(defn- render-text
[props]
(mf/html
[:> editor-text-node props]))
;; --- Text Shape Edit
(defn- initial-text
[text]
(clj->js
[{:type "root"
:children [{:type "paragraph-set"
:children [{:type "paragraph"
:children [{:text (or text "")}]}]}]}]))
(defn- parse-content
[content]
(cond
(string? content) (initial-text content)
(map? content) (clj->js [content])
:else (initial-text "")))
(defn- content-size
[node]
(let [current (count (:text node))
children-count (->> node :children (map content-size) (reduce +))]
(+ current children-count)))
(defn fix-gradients
"Fix for the gradient types that need to be keywords"
[content]
(let [fix-node
(fn [node]
(d/update-in-when node [:fill-color-gradient :type] keyword))]
(ut/map-node fix-node content)))
(mf/defc text-shape-edit
{::mf/wrap [mf/memo]}
[{:keys [shape read-only?] :or {read-only? false} :as props}]
(let [{:keys [id x y width height content grow-type]} shape
zoom (mf/deref refs/selected-zoom)
state (mf/use-state #(parse-content content))
editor (mf/use-memo #(dwt/create-editor))
self-ref (mf/use-ref)
selecting-ref (mf/use-ref)
measure-ref (mf/use-ref)
content-var (mf/use-var content)
on-close
(fn []
(when (not read-only?)
(st/emit! dw/clear-edition-mode))
(when (= 0 (content-size @content-var))
(st/emit! (dw/delete-shapes [id]))))
on-click-outside
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [sidebar (dom/get-element "settings-bar")
assets (dom/get-element-by-class "assets-bar")
cpicker (dom/get-element-by-class "colorpicker-tooltip")
self (mf/ref-val self-ref)
target (dom/get-target event)
selecting? (mf/ref-val selecting-ref)]
(when-not (or (and sidebar (.contains sidebar target))
(and assets (.contains assets target))
(and self (.contains self target))
(and cpicker (.contains cpicker target)))
(if selecting?
(mf/set-ref-val! selecting-ref false)
(on-close)))))
on-mouse-down
(fn [event]
(mf/set-ref-val! selecting-ref true))
on-mouse-up
(fn [event]
(mf/set-ref-val! selecting-ref false))
on-key-up
(fn [event]
(dom/stop-propagation event)
(when (= (.-keyCode event) 27) ; ESC
(do
(st/emit! :interrupt)
(on-close))))
on-mount
(fn []
(when (not read-only?)
(let [lkey1 (events/listen (dom/get-root) EventType.CLICK on-click-outside)
lkey2 (events/listen (dom/get-root) EventType.KEYUP on-key-up)]
(st/emit! (dwt/assign-editor id editor)
dwc/start-undo-transaction)
#(do
(st/emit! (dwt/assign-editor id nil)
dwc/commit-undo-transaction)
(events/unlistenByKey lkey1)
(events/unlistenByKey lkey2)))))
on-focus
(fn [event]
(when (not read-only?)
(dwt/editor-select-all! editor)))
on-change
(mf/use-callback
(fn [val]
(when (not read-only?)
(let [content (js->clj val :keywordize-keys true)
content (first content)
content (fix-gradients content)]
;; Append timestamp so we can react to cursor change events
(st/emit! (dw/update-shape id {:content (assoc content :ts (js->clj (.now js/Date)))}))
(reset! state val)
(reset! content-var content)))))]
(mf/use-effect on-mount)
(mf/use-effect
(mf/deps content)
(fn []
(reset! state (parse-content content))
(reset! content-var content)))
;; Checks the size of the wrapper to update if it were necesary
(mf/use-effect
(mf/deps shape)
(fn []
(fonts/ready
#(let [self-node (mf/ref-val self-ref)
paragraph-node (when self-node (dom/query self-node ".paragraph-set"))]
(when paragraph-node
(let [
{bb-w :width bb-h :height} (dom/get-bounding-rect paragraph-node)
width (max (/ bb-w zoom) 7)
height (max (/ bb-h zoom) 16)
undo-transaction (get-in @st/state [:workspace-undo :transaction])]
(when (not undo-transaction) (st/emit! dwc/start-undo-transaction))
(when (or (not= (:width shape) width)
(not= (:height shape) height))
(cond
(and (:overflow-text shape) (not= :fixed (:grow-type shape)))
(st/emit! (dwt/update-overflow-text id false))
(and (= :fixed (:grow-type shape)) (not (:overflow-text shape)) (> height (:height shape)))
(st/emit! (dwt/update-overflow-text id true))
(and (= :fixed (:grow-type shape)) (:overflow-text shape) (<= height (:height shape)))
(st/emit! (dwt/update-overflow-text id false))
(= grow-type :auto-width)
(st/emit! (dw/update-dimensions [id] :width width)
(dw/update-dimensions [id] :height height))
(= grow-type :auto-height)
(st/emit! (dw/update-dimensions [id] :height height))
))
(when (not undo-transaction) (st/emit! dwc/discard-undo-transaction))))))))
[:foreignObject {:ref self-ref
:transform (geom/transform-matrix shape)
:x x :y y
:width (if (= :auto-width grow-type) 10000 width)
:height height}
[:style "span { line-height: inherit; }
.text-node { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
[:> rslate/Slate {:editor editor
:value @state
:on-change on-change}
[:> rslate/Editable
{:auto-focus (when (not read-only?) "true")
:spell-check "false"
:on-focus on-focus
:class "rich-text"
:style {:cursor cur/text
:width (:width shape)}
:render-element #(render-element shape %)
:render-leaf render-text
:on-mouse-up on-mouse-up
:on-mouse-down on-mouse-down
:on-blur (fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
;; WARN: monky patch
(obj/set! slate/Transforms "deselect" (constantly nil)))
:placeholder (when (= :fixed grow-type) "Type some text here...")}]]]))

View file

@ -0,0 +1,260 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.text.editor
(:require
["slate" :as slate]
["slate-react" :as rslate]
[goog.events :as events]
[rumext.alpha :as mf]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.util.dom :as dom]
[app.util.text :as ut]
[app.util.object :as obj]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.fonts :as fonts]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.texts :as dwt]
[app.main.ui.cursors :as cur]
[app.main.ui.shapes.text.styles :as sts])
(:import
goog.events.EventType
goog.events.KeyCodes))
;; --- Data functions
(defn- initial-text
[text]
(clj->js
[{:type "root"
:children [{:type "paragraph-set"
:children [{:type "paragraph"
:children [{:text (or text "")}]}]}]}]))
(defn- parse-content
[content]
(cond
(string? content) (initial-text content)
(map? content) (clj->js [content])
:else (initial-text "")))
(defn- content-size
[node]
(let [current (count (:text node))
children-count (->> node :children (map content-size) (reduce +))]
(+ current children-count)))
(defn- fix-gradients
"Fix for the gradient types that need to be keywords"
[content]
(let [fix-node
(fn [node]
(d/update-in-when node [:fill-color-gradient :type] keyword))]
(ut/map-node fix-node content)))
;; --- Text Editor Rendering
(mf/defc editor-root-node
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [
childs (obj/get props "children")
data (obj/get props "element")
type (obj/get data "type")
style (sts/generate-root-styles data props)
attrs (-> (obj/get props "attributes")
(obj/set! "style" style)
(obj/set! "className" type))]
[:> :div attrs childs]))
(mf/defc editor-paragraph-set-node
{::mf/wrap-props false}
[props]
(let [childs (obj/get props "children")
data (obj/get props "element")
type (obj/get data "type")
shape (obj/get props "shape")
style (sts/generate-paragraph-set-styles data)
attrs (-> (obj/get props "attributes")
(obj/set! "style" style)
(obj/set! "className" type))]
[:> :div attrs childs]))
(mf/defc editor-paragraph-node
{::mf/wrap-props false}
[props]
(let [
childs (obj/get props "children")
data (obj/get props "element")
type (obj/get data "type")
style (sts/generate-paragraph-styles data)
attrs (-> (obj/get props "attributes")
(obj/set! "style" style)
(obj/set! "className" type))]
[:> :p attrs childs]))
(mf/defc editor-text-node
{::mf/wrap-props false}
[props]
(let [childs (obj/get props "children")
data (obj/get props "leaf")
style (sts/generate-text-styles data)
attrs (-> (obj/get props "attributes")
(obj/set! "style" style)
(obj/set! "className" "text-node"))]
[:> :span attrs childs]))
(defn- render-element
[shape props]
(mf/html
(let [element (obj/get props "element")
type (obj/get element "type")
props (obj/merge! props #js {:shape shape})
props (cond-> props
(= type "root") (obj/set! "key" "root")
(= type "paragraph-set") (obj/set! "key" "paragraph-set"))]
(case type
"root" [:> editor-root-node props]
"paragraph-set" [:> editor-paragraph-set-node props]
"paragraph" [:> editor-paragraph-node props]
nil))))
(defn- render-text
[props]
(mf/html
[:> editor-text-node props]))
;; --- Text Shape Edit
(mf/defc text-shape-edit
{::mf/wrap [mf/memo]
::mf/wrap-props false
::mf/forward-ref true}
[props ref]
(let [shape (unchecked-get props "shape")
node-ref (unchecked-get props "node-ref")
{:keys [id x y width height content grow-type]} shape
zoom (mf/deref refs/selected-zoom)
state (mf/use-state #(parse-content content))
editor (mf/use-memo #(dwt/create-editor))
self-ref (mf/use-ref)
selecting-ref (mf/use-ref)
measure-ref (mf/use-ref)
content-var (mf/use-var content)
on-close
(fn []
(st/emit! dw/clear-edition-mode)
(when (= 0 (content-size @content-var))
(st/emit! (dw/delete-shapes [id]))))
on-click-outside
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [sidebar (dom/get-element "settings-bar")
assets (dom/get-element-by-class "assets-bar")
cpicker (dom/get-element-by-class "colorpicker-tooltip")
self (mf/ref-val self-ref)
target (dom/get-target event)
selecting? (mf/ref-val selecting-ref)]
(when-not (or (and sidebar (.contains sidebar target))
(and assets (.contains assets target))
(and self (.contains self target))
(and cpicker (.contains cpicker target)))
(if selecting?
(mf/set-ref-val! selecting-ref false)
(on-close)))))
on-mouse-down
(fn [event]
(mf/set-ref-val! selecting-ref true))
on-mouse-up
(fn [event]
(mf/set-ref-val! selecting-ref false))
on-key-up
(fn [event]
(dom/stop-propagation event)
(when (= (.-keyCode event) 27) ; ESC
(do
(st/emit! :interrupt)
(on-close))))
on-mount
(fn []
(let [lkey1 (events/listen (dom/get-root) EventType.CLICK on-click-outside)
lkey2 (events/listen (dom/get-root) EventType.KEYUP on-key-up)]
(st/emit! (dwt/assign-editor id editor)
dwc/start-undo-transaction)
#(do
(st/emit! (dwt/assign-editor id nil)
dwc/commit-undo-transaction)
(events/unlistenByKey lkey1)
(events/unlistenByKey lkey2))))
on-focus
(fn [event]
(dwt/editor-select-all! editor))
on-change
(mf/use-callback
(fn [val]
(let [content (js->clj val :keywordize-keys true)
content (first content)
content (fix-gradients content)]
;; Append timestamp so we can react to cursor change events
(st/emit! (dw/update-shape id {:content (assoc content :ts (js->clj (.now js/Date)))}))
(reset! state val)
(reset! content-var content))))]
(mf/use-effect on-mount)
(mf/use-effect
(mf/deps content)
(fn []
(reset! state (parse-content content))
(reset! content-var content)))
[:foreignObject {:ref self-ref
:transform (gsh/transform-matrix shape)
:x x :y y
:width (if (#{:auto-width} grow-type) 10000 width)
:height (if (#{:auto-height :auto-width} grow-type) 10000 height)}
[:style "span { line-height: inherit; }
.text-node { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
[:> rslate/Slate {:editor editor
:value @state
:on-change on-change}
[:> rslate/Editable
{:auto-focus "true"
:spell-check "false"
:on-focus on-focus
:class "rich-text"
:style {:cursor cur/text
:width (:width shape)}
:render-element #(render-element shape %)
:render-leaf render-text
:on-mouse-up on-mouse-up
:on-mouse-down on-mouse-down
:on-blur (fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
;; WARN: monky patch
(obj/set! slate/Transforms "deselect" (constantly nil)))
:placeholder (when (= :fixed grow-type) "Type some text here...")}]]]))

View file

@ -53,7 +53,7 @@
[potok.core :as ptk]
[promesa.core :as p]
[rumext.alpha :as mf]
[app.main.ui.workspace.shapes.path :refer [path-actions]])
[app.main.ui.workspace.shapes.path.actions :refer [path-actions]])
(:import goog.events.EventType))
;; --- Coordinates Widget