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:
commit
53297ec9d9
29 changed files with 1273 additions and 1198 deletions
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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)}]]))
|
||||
|
|
75
frontend/src/app/main/ui/shapes/text/embed.cljs
Normal file
75
frontend/src/app/main/ui/shapes/text/embed.cljs
Normal 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])))
|
119
frontend/src/app/main/ui/shapes/text/styles.cljs
Normal file
119
frontend/src/app/main/ui/shapes/text/styles.cljs
Normal 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))
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
|
|
78
frontend/src/app/main/ui/workspace/effects.cljs
Normal file
78
frontend/src/app/main/ui/workspace/effects.cljs
Normal 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))))))))))
|
|
@ -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)]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}]])))
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]])]))
|
||||
|
|
47
frontend/src/app/main/ui/workspace/shapes/path/actions.cljs
Normal file
47
frontend/src/app/main/ui/workspace/shapes/path/actions.cljs
Normal 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]]]))
|
39
frontend/src/app/main/ui/workspace/shapes/path/common.cljs
Normal file
39
frontend/src/app/main/ui/workspace/shapes/path/common.cljs
Normal 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))))
|
||||
|
235
frontend/src/app/main/ui/workspace/shapes/path/editor.cljs
Normal file
235
frontend/src/app/main/ui/workspace/shapes/path/editor.cljs
Normal 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}]])]))
|
||||
|
|
@ -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...")}]]]))
|
||||
|
|
260
frontend/src/app/main/ui/workspace/shapes/text/editor.cljs
Normal file
260
frontend/src/app/main/ui/workspace/shapes/text/editor.cljs
Normal 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...")}]]]))
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue