From 2a17f0e5070e69ac8e9991c95a3cb75d779248c7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 27 Nov 2020 07:30:12 +0100 Subject: [PATCH] :recycle: Refactor the text size calculations --- frontend/deps.edn | 2 +- frontend/src/app/main/data/workspace.cljs | 2 +- .../src/app/main/data/workspace/common.cljs | 52 +- frontend/src/app/main/fonts.cljs | 5 + frontend/src/app/main/ui/shapes/text.cljs | 233 ++------ .../src/app/main/ui/shapes/text/embed.cljs | 75 +++ .../src/app/main/ui/shapes/text/styles.cljs | 119 ++++ .../src/app/main/ui/workspace/drawarea.cljs | 2 +- .../src/app/main/ui/workspace/effects.cljs | 78 +++ .../src/app/main/ui/workspace/selection.cljs | 2 +- .../src/app/main/ui/workspace/shapes.cljs | 24 +- .../app/main/ui/workspace/shapes/common.cljs | 70 +-- .../app/main/ui/workspace/shapes/frame.cljs | 99 ++-- .../app/main/ui/workspace/shapes/group.cljs | 54 +- .../app/main/ui/workspace/shapes/path.cljs | 330 +---------- .../ui/workspace/shapes/path/actions.cljs | 47 ++ .../main/ui/workspace/shapes/path/common.cljs | 39 ++ .../main/ui/workspace/shapes/path/editor.cljs | 235 ++++++++ .../app/main/ui/workspace/shapes/text.cljs | 515 ++++-------------- .../main/ui/workspace/shapes/text/editor.cljs | 256 +++++++++ .../src/app/main/ui/workspace/viewport.cljs | 2 +- 21 files changed, 1128 insertions(+), 1113 deletions(-) create mode 100644 frontend/src/app/main/ui/shapes/text/embed.cljs create mode 100644 frontend/src/app/main/ui/shapes/text/styles.cljs create mode 100644 frontend/src/app/main/ui/workspace/effects.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/path/actions.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/path/common.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/path/editor.cljs create mode 100644 frontend/src/app/main/ui/workspace/shapes/text/editor.cljs diff --git a/frontend/deps.edn b/frontend/deps.edn index 072351097..2e79e6972 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -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]} diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5c49f5609..3cc6bb702 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 78e69567d..69a50ad6a 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -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 diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index a20d94d6e..61e704537 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -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)))))) diff --git a/frontend/src/app/main/ui/shapes/text.cljs b/frontend/src/app/main/ui/shapes/text.cljs index 4a4513e01..2d36fe781 100644 --- a/frontend/src/app/main/ui/shapes/text.cljs +++ b/frontend/src/app/main/ui/shapes/text.cljs @@ -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)}]])) diff --git a/frontend/src/app/main/ui/shapes/text/embed.cljs b/frontend/src/app/main/ui/shapes/text/embed.cljs new file mode 100644 index 000000000..9d41810f0 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/embed.cljs @@ -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]))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs new file mode 100644 index 000000000..1dfaef189 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/drawarea.cljs b/frontend/src/app/main/ui/workspace/drawarea.cljs index 5cb752341..c89a4e691 100644 --- a/frontend/src/app/main/ui/workspace/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/drawarea.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/effects.cljs b/frontend/src/app/main/ui/workspace/effects.cljs new file mode 100644 index 000000000..4cf434674 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/effects.cljs @@ -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)))))))))) diff --git a/frontend/src/app/main/ui/workspace/selection.cljs b/frontend/src/app/main/ui/workspace/selection.cljs index 54787f1e9..5ea3d5ea7 100644 --- a/frontend/src/app/main/ui/workspace/selection.cljs +++ b/frontend/src/app/main/ui/workspace/selection.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index bb6e9603f..a8d65e2e9 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -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] diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index af268cd69..168bd0dd6 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -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}]]))) - - diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 1d84fa620..d092b48d9 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index bb91982dd..3e265d25c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 09a3ab9b2..420510178 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -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}]])])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs new file mode 100644 index 000000000..4f41e9d57 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/path/actions.cljs @@ -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]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path/common.cljs b/frontend/src/app/main/ui/workspace/shapes/path/common.cljs new file mode 100644 index 000000000..b5f408c92 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/path/common.cljs @@ -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)))) + diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs new file mode 100644 index 000000000..1cefba76a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -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}]])])) + diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 2a9b1b24b..f6b08211d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -9,455 +9,132 @@ (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.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 [name 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) + (or (not= shape-width new-width) + (not= shape-height new-height))) + (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)) + + (= grow-type :auto-width) + (st/emit! (dw/update-dimensions [id] :width new-width) + (dw/update-dimensions [id] :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 [shape root query] + (mf/use-effect + (mf/deps shape 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 width 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) + edit-text-ref (mf/use-ref nil) + edit-text-node (mf/ref-val edit-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") + (resize-observer shape edit-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" + :ref edit-text-ref + :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...")}]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs new file mode 100644 index 000000000..7aa43d8e6 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -0,0 +1,256 @@ +;; 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") + 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 + +(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") + ;; read-only? (or (unchecked-get props "read-only?") false) + + {: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 (when node-ref (mf/ref-val node-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 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...")}]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index de145248a..20d384a60 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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