diff --git a/CHANGES.md b/CHANGES.md index 4b20564be..0b5c020f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,10 +46,13 @@ - Add the ability to disable standard, password login [Taiga #2999](https://tree.taiga.io/project/penpot/us/2999) - Don't stop SVG import when an image cannot be imported [#1531](https://github.com/penpot/penpot/issues/1531) - Fix paste shapes while editing text [Taiga #2396](https://tree.taiga.io/project/penpot/issue/2396) +- Show Penpot color in Safari tab bar [#1803](https://github.com/penpot/penpot/issues/1803) ### :bug: Bugs fixed - Round the size values on handoff to two decimals [Taiga #3227](https://tree.taiga.io/project/penpot/issue/3227) +- Fix internal error when hoverin over shape [Taiga #3237](https://tree.taiga.io/project/penpot/issue/3237) +- Fix different behaviour during image drag [Taiga #2279](https://tree.taiga.io/project/penpot/issue/2279) - Fix hidden file name on import [Taiga #3172](https://tree.taiga.io/project/penpot/issue/3172) - Fix unneccessary scrollbars at the color list [Taiga #3211](https://tree.taiga.io/project/penpot/issue/3211) - "Show in exports" is showing in multiselections [Taiga #3194](https://tree.taiga.io/project/penpot/issue/3194) diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc index 7d5cb5e51..95dbd63b6 100644 --- a/common/src/app/common/colors.cljc +++ b/common/src/app/common/colors.cljc @@ -14,6 +14,7 @@ (def gray-20 "#B1B2B5") (def gray-30 "#7B7D85") (def gray-40 "#64666A") +(def gray-50 "#303236") (def info "#59B9E2") (def test "#fabada") (def white "#FFFFFF") diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index af628cb0b..bdf9882b2 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -34,8 +34,12 @@ `(-> ~target ~@(map (fn [key] (list `c/get key)) keys))) ([target keys default] (assert (vector? keys) "keys expected to be a vector") - `(let [v# (-> ~target ~@(map (fn [key] (list `c/get key)) keys))] - (if (some? v#) v# ~default)))) + (let [last-index (dec (count keys))] + `(-> ~target ~@(map-indexed (fn [index key] + (if (= last-index index) + (list `c/get key default) + (list `c/get key))) + keys))))) (defmacro str [& params] diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index afc1bb6bb..e201d3181 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -133,6 +133,7 @@ (dm/export gtr/transform-bounds) (dm/export gtr/modifiers->transform) (dm/export gtr/empty-modifiers?) +(dm/export gtr/move-position-data) ;; Constratins (dm/export gct/calc-child-modifiers) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index 7e2970cb3..c9f2e33df 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -134,10 +134,8 @@ (defn write-log! [logger level exception message] - (let [message (if (string? message) - message - (str/join ", " message))] - #?(:clj + #?(:clj + (let [message (if (string? message) message (str/join ", " message))] (if exception (.log ^Logger logger ^Level level @@ -145,14 +143,17 @@ ^Throwable exception) (.log ^Logger logger ^Level level - ^Object message)) - :cljs - (when glog/ENABLED - (when-let [l (get-logger logger)] - (let [level (get-level level) - record (glog/LogRecord. level message (.getName ^js l))] + ^Object message))) + :cljs + (when glog/ENABLED + (let [logger (get-logger logger) + level (get-level level)] + (when (and logger (glog/isLoggable logger level)) + (let [message (if (fn? message) (message) message) + message (if (string? message) message (str/join ", " message)) + record (glog/LogRecord. level message (.getName ^js logger))] (when exception (.setException record exception)) - (glog/publishLogRecord l record))))))) + (glog/publishLogRecord logger record))))))) #?(:clj (defn enabled? @@ -174,9 +175,8 @@ (defmacro log [& props] (if (:ns &env) ; CLJS - (let [{:keys [level cause ::logger ::raw]} props - message (or raw (build-message props))] - `(write-log! ~(or logger (str *ns*)) ~level ~cause (or ~raw (build-message ~(vec props))))) + (let [{:keys [level cause ::logger ::raw]} props] + `(write-log! ~(or logger (str *ns*)) ~level ~cause (or ~raw (fn [] (build-message ~(vec props)))))) (let [{:keys [level cause ::logger ::async ::raw ::context] :or {async true}} props logger (or logger (str *ns*)) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index b042d1c2d..42a4c76ac 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -139,6 +139,18 @@ (:shapes) (keep lookup))))) +(defn get-frames-ids + "Retrieves all frame objects as vector. It is not implemented in + function of `get-immediate-children` for performance reasons. This + function is executed in the render hot path." + [objects] + (let [lookup (d/getf objects) + xform (comp (keep lookup) + (filter frame-shape?) + (map :id))] + (->> (:shapes (lookup uuid/zero)) + (into [] xform)))) + (defn get-frames "Retrieves all frame objects as vector. It is not implemented in function of `get-immediate-children` for performance reasons. This @@ -468,3 +480,25 @@ (let [path-split (split-path path)] (merge-path-item (first path-split) name))) + +(defn get-frame-objects + "Retrieves a new objects map only with the objects under frame-id (with frame-id)" + [objects frame-id] + (let [ids (concat [frame-id] (get-children-ids objects frame-id))] + (select-keys objects ids))) + +(defn objects-by-frame + "Returns a map of the `objects` grouped by frame. Every value of the map has + the same format as objects id->shape-data" + [objects] + ;; Implemented with transients for performance. 30~50% better + (letfn [(process-shape [objects [id shape]] + (let [frame-id (if (= :frame (:type shape)) id (:frame-id shape)) + cur (-> (or (get objects frame-id) (transient {})) + (assoc! id shape))] + (assoc! objects frame-id cur)))] + (d/update-vals + (->> objects + (reduce process-shape (transient {})) + (persistent!)) + persistent!))) diff --git a/common/src/app/common/path/commands.cljc b/common/src/app/common/path/commands.cljc index a79b34676..5bae5862b 100644 --- a/common/src/app/common/path/commands.cljc +++ b/common/src/app/common/path/commands.cljc @@ -98,7 +98,6 @@ (let [content (if (vector? content) content (into [] content))] (reduce apply-to-index content modifiers)))) - (defn get-handler [{:keys [params] :as command} prefix] (let [cx (d/prefix-keyword prefix :x) cy (d/prefix-keyword prefix :y)] diff --git a/frontend/deps.edn b/frontend/deps.edn index d4dd3597c..231b30c56 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -10,7 +10,7 @@ funcool/beicon {:mvn/version "2021.07.05-1"} funcool/okulary {:mvn/version "2022.04.11-16"} funcool/potok {:mvn/version "2021.09.20-0"} - funcool/rumext {:mvn/version "2022.03.31-133"} + funcool/rumext {:mvn/version "2022.04.19-148"} funcool/tubax {:mvn/version "2021.05.20-0"} instaparse/instaparse {:mvn/version "1.4.10"} diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index bf6ab809d..ba8c3c054 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -15,6 +15,7 @@ + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index bbed408dd..eabcaced0 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -43,6 +43,7 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] + [app.main.data.workspace.thumbnails :as dwth] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.zoom :as dwz] @@ -195,7 +196,8 @@ ptk/WatchEvent (watch [_ state _] (if (contains? (get-in state [:workspace-data :pages-index]) page-id) - (rx/of (dwp/preload-data-uris)) + (rx/of (dwp/preload-data-uris) + (dwth/watch-state-changes)) (let [default-page-id (get-in state [:workspace-data :pages 0])] (rx/of (go-to-page default-page-id))))) @@ -1767,3 +1769,7 @@ (dm/export dwz/decrease-zoom) (dm/export dwz/increase-zoom) (dm/export dwz/set-zoom) + +;; Thumbnails +(dm/export dwth/update-thumbnail) + diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 4eb4763de..fa8c00f51 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -160,8 +160,8 @@ ;; This stream checks the consecutive mouse positions to do the dragging (->> points (streams/move-points-stream snap-toggled start-position selected-points) - (rx/take-until stopper) - (rx/map #(move-selected-path-point start-position %))) + (rx/map #(move-selected-path-point start-position %)) + (rx/take-until stopper)) (rx/of (apply-content-modifiers))))))) (defn- get-displacement diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index df6366a6d..47be93517 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -50,7 +50,7 @@ id (get-in state [:workspace-local :edition]) content (st/get-path state :content) selected-point? #(gsh/has-point-rect? selrect %) - selected-points (get-in state [:workspace-local :edit-path id :selected-points]) + selected-points (or (get-in state [:workspace-local :edit-path id :selected-points]) #{}) positions (into (if shift? selected-points #{}) (comp (filter #(not (= (:command %) :close-path))) (map (comp gpt/point :params)) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 1069d00a8..c4fb4c05b 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -9,7 +9,6 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.pages :as cp] - [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.spec.change :as spec.change] [app.common.spec.file :as spec.file] @@ -26,7 +25,6 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.svg-upload :as svg] - [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.util.http :as http] @@ -35,7 +33,6 @@ [app.util.uri :as uu] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] [cuerdas.core :as str] [potok.core :as ptk] [promesa.core :as p] @@ -552,136 +549,6 @@ (update-in [:workspace-file :pages] #(filterv (partial not= id) %)) (update :workspace-pages dissoc id))) -(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail)) - -(defn remove-thumbnails - [ids] - (ptk/reify ::remove-thumbnails - ptk/WatchEvent - (watch [_ state _] - ;; Removes the thumbnail while it's regenerated - (let [moving? (= :move (get-in state [:workspace-local :transform])) - selected? (wsh/lookup-selected state) - ;; When we're moving the current frame it's safe to keep the thumbnail - ;; if it's resize we need to remove it immeditely - ids (cond->> ids moving? (remove selected?))] - - (if (empty? ids) - (rx/empty) - (rx/of (dch/update-shapes ids #(dissoc % :thumbnail) {:save-undo? false}))))))) - -(defn update-frame-thumbnail - [frame-id] - (ptk/event ::update-frame-thumbnail {:frame-id frame-id})) - -(defn update-shape-thumbnail - "An event that is succeptible to be executed out of the main flow, so - it need to correctly handle the situation that there are no page-id - or file-is loaded." - [shape-id thumbnail-data] - (ptk/reify ::update-shape-thumbnail - ptk/WatchEvent - (watch [_ state _] - (when (and (dwc/initialized? state) - (uuid? shape-id)) - (rx/of (dch/update-shapes [shape-id] - #(assoc % :thumbnail thumbnail-data) - {:save-undo? false})))))) - -(defn- extract-frame-changes - "Process a changes set in a commit to extract the frames that are changing" - [[event [old-objects new-objects]]] - (let [changes (-> event deref :changes) - - extract-ids - (fn [{type :type :as change}] - (case type - :add-obj [(:id change)] - :mod-obj [(:id change)] - :del-obj [(:id change)] - :reg-objects (:shapes change) - :mov-objects (:shapes change) - [])) - - get-frame-id - (fn [id] - (let [shape (or (get new-objects id) - (get old-objects id))] - - (or (and (= :frame (:type shape)) id) - (:frame-id shape)))) - - ;; Extracts the frames and then removes nils and the root frame - xform (comp (mapcat extract-ids) - (map get-frame-id) - (remove nil?) - (filter #(not= uuid/zero %)) - (filter #(contains? new-objects %)))] - - (into #{} xform changes))) - -(defn thumbnail-change? - "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" - [event] - (let [changes (-> event deref :changes) - - is-thumbnail-op? - (fn [{type :type attr :attr}] - (and (= type :set) - (= attr :thumbnail))) - - is-thumbnail-change? - (fn [change] - (and (= (:type change) :mod-obj) - (->> change :operations (every? is-thumbnail-op?))))] - - (->> changes (every? is-thumbnail-change?)))) - -(defn watch-state-changes [] - (ptk/reify ::watch-state-changes - ptk/WatchEvent - (watch [_ state stream] - (let [stopper (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) - (= ::watch-state-changes (ptk/type %))))) - - objects-stream (->> (rx/concat - (rx/of nil) - (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) - ;; We need to keep the old-objects so we can check the frame for the - ;; deleted objects - (rx/buffer 2 1)) - - frame-changes (->> stream - (rx/filter dch/commit-changes?) - - ;; Async so we wait for additional side-effects of commit-changes - (rx/observe-on :async) - (rx/filter (comp not thumbnail-change?)) - (rx/with-latest-from objects-stream) - (rx/map extract-frame-changes) - (rx/share)) - - frames (-> state wsh/lookup-page-objects cph/get-frames) - no-thumb-frames (->> frames - (filter (comp nil? :thumbnail)) - (mapv :id))] - - (rx/concat - (->> (rx/from no-thumb-frames) - (rx/map #(update-frame-thumbnail %))) - - ;; We remove the thumbnails immediately but defer their generation - (rx/merge - (->> frame-changes - (rx/take-until stopper) - (rx/map #(remove-thumbnails %))) - - (->> frame-changes - (rx/take-until stopper) - (rx/buffer-until (->> frame-changes (rx/debounce 1000))) - (rx/flat-map #(reduce set/union %)) - (rx/map #(update-frame-thumbnail %))))))))) (defn preload-data-uris "Preloads the image data so it's ready when necesary" diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index ff79bcb7e..6cc5aa6c4 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -21,6 +21,7 @@ [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.state-helpers :as wsh] + [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.zoom :as dwz] [app.main.refs :as refs] [app.main.streams :as ms] @@ -495,18 +496,30 @@ id-original (first selected) - selected (->> changes + new-selected (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (filter #(selected (:old-id %))) (map #(get-in % [:obj :id])) (into (d/ordered-set))) - id-duplicated (first selected)] + dup-frames (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(selected (:old-id %))) + (filter #(= :frame (get-in % [:obj :type]))) + (map #(vector (:old-id %) (get-in % [:obj :id])))) + + id-duplicated (first new-selected)] + + (rx/concat + (->> (rx/from dup-frames) + (rx/map (fn [[old-id new-id]] (dwt/duplicate-thumbnail old-id new-id)))) + ;; Warning: This order is important for the focus mode. - (rx/of (dch/commit-changes changes) - (select-shapes selected) - (memorize-duplicated id-original id-duplicated))))))))) + (rx/of (dch/commit-changes changes) + (select-shapes new-selected) + (memorize-duplicated id-original id-duplicated)))))))))) (defn change-hover-state [id value] diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 549148bb9..5c523e638 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -74,6 +74,7 @@ ;; Color present as attribute (uc/color? (str/trim (get-in shape [:svg-attrs :fill]))) (-> (update :svg-attrs dissoc :fill) + (update-in [:svg-attrs :style] dissoc :fill) (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :fill]) (str/trim) (uc/parse-color)))) @@ -81,17 +82,20 @@ ;; Color present as style (uc/color? (str/trim (get-in shape [:svg-attrs :style :fill]))) (-> (update-in [:svg-attrs :style] dissoc :fill) + (update :svg-attrs dissoc :fill) (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :style :fill]) (str/trim) (uc/parse-color)))) (get-in shape [:svg-attrs :fill-opacity]) (-> (update :svg-attrs dissoc :fill-opacity) + (update-in [:svg-attrs :style] dissoc :fill-opacity) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) (d/parse-double)))) (get-in shape [:svg-attrs :style :fill-opacity]) (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) + (update :svg-attrs dissoc :fill-opacity) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) (d/parse-double)))))) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 7a1ccbcb0..930b7fc99 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -8,6 +8,7 @@ (:require [app.common.attrs :as attrs] [app.common.data :as d] + [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.pages.helpers :as cph] @@ -303,83 +304,27 @@ (defn not-changed? [old-dim new-dim] (> (mth/abs (- old-dim new-dim)) 0.1)) -(defn resize-text-batch [changes] - (ptk/reify ::resize-text-batch - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data :pages-index page-id :objects])] - (if-not (every? #(contains? objects(first %)) changes) - (rx/empty) - - (let [changes-map (->> changes (into {})) - ids (keys changes-map) - update-fn - (fn [shape] - (let [[new-width new-height] (get changes-map (:id shape)) - {:keys [selrect grow-type]} (gsh/transform-shape shape) - {shape-width :width shape-height :height} selrect - - modifier-width (gsh/resize-modifiers shape :width new-width) - modifier-height (gsh/resize-modifiers shape :height new-height)] - - (cond-> shape - (and (not-changed? shape-width new-width) (= grow-type :auto-width)) - (-> (assoc :modifiers modifier-width) - (gsh/transform-shape)) - - (and (not-changed? shape-height new-height) - (or (= grow-type :auto-height) (= grow-type :auto-width))) - (-> (assoc :modifiers modifier-height) - (gsh/transform-shape)))))] - - (rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))) - -;; When a resize-event arrives we start "buffering" for a time -;; after that time we invoke `resize-text-batch` with all the changes -;; together. This improves the performance because we only re-render the -;; resized components once even if there are changes that applies to -;; lots of texts like changing a font (defn resize-text [id new-width new-height] (ptk/reify ::resize-text - IDeref - (-deref [_] - {:id id :width new-width :height new-height}) - ptk/WatchEvent - (watch [_ state stream] - (let [;; This stream aggregates the events of "resizing" - resize-events - (rx/merge - (->> (rx/of (resize-text id new-width new-height))) - (->> stream (rx/filter (ptk/type? ::resize-text)))) + (watch [_ _ _] + (letfn [(update-fn [shape] + (let [{:keys [selrect grow-type]} shape + {shape-width :width shape-height :height} selrect + modifier-width (gsh/resize-modifiers shape :width new-width) + modifier-height (gsh/resize-modifiers shape :height new-height)] + (cond-> shape + (and (not-changed? shape-width new-width) (= grow-type :auto-width)) + (-> (assoc :modifiers modifier-width) + (gsh/transform-shape)) - ;; Stop buffering after time without resizes - stop-buffer (->> resize-events (rx/debounce 100)) + (and (not-changed? shape-height new-height) + (or (= grow-type :auto-height) (= grow-type :auto-width))) + (-> (assoc :modifiers modifier-height) + (gsh/transform-shape)))))] - ;; Aggregates the resizes so only send the resize when the sizes are stable - resize-batch - (->> resize-events - (rx/take-until stop-buffer) - (rx/reduce (fn [acc event] - (assoc acc (:id @event) [(:width @event) (:height @event)])) - {id [new-width new-height]}) - (rx/map #(resize-text-batch %))) - - ;; This stream retrieves the changes of page so we cancel the agregation - change-page - (->> stream - (rx/filter (ptk/type? :app.main.data.workspace/finalize-page)) - (rx/take 1) - (rx/ignore))] - - (if-not (::handling-texts state) - (->> (rx/concat - (rx/of #(assoc % ::handling-texts true)) - (rx/race resize-batch change-page) - (rx/of #(dissoc % ::handling-texts)))) - (rx/empty)))))) + (rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false})))))) (defn save-font [data] @@ -391,3 +336,46 @@ (not multiple?) (assoc-in [:workspace-global :default-font] data)))))) +(defn apply-text-modifier + [shape {:keys [width height position-data]}] + + (let [modifier-width (when width (gsh/resize-modifiers shape :width width)) + modifier-height (when height (gsh/resize-modifiers shape :height height)) + + new-shape + (cond-> shape + (some? modifier-width) + (-> (assoc :modifiers modifier-width) + (gsh/transform-shape)) + + (some? modifier-height) + (-> (assoc :modifiers modifier-height) + (gsh/transform-shape)) + + (some? position-data) + (assoc :position-data position-data)) + + delta-move + (gpt/subtract (gpt/point (:selrect new-shape)) + (gpt/point (:selrect shape))) + + + new-shape + (update new-shape :position-data gsh/move-position-data (:x delta-move) (:y delta-move))] + + + new-shape)) + +(defn update-text-modifier + [id props] + (ptk/reify ::update-text-modifier + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-text-modifier id] (fnil merge {}) props)))) + +(defn remove-text-modifier + [id] + (ptk/reify ::remove-text-modifier + ptk/UpdateEvent + (update [_ state] + (d/dissoc-in state [:workspace-text-modifier id])))) diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs new file mode 100644 index 000000000..a0ed270e4 --- /dev/null +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -0,0 +1,171 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.thumbnails + (:require + [app.common.data :as d] + [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.refs :as refs] + [app.main.repo :as rp] + [app.main.store :as st] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn force-render-stream + "Stream that will inform the frame-wrapper to mount into memory" + [id] + (->> st/stream + (rx/filter (ptk/type? ::force-render)) + (rx/map deref) + (rx/filter #(= % id)) + (rx/take 1))) + +(defn update-thumbnail + "Updates the thumbnail information for the given frame `id`" + [id data] + (let [lock (uuid/next)] + (ptk/reify ::update-thumbnail + IDeref + (-deref [_] {:id id :data data}) + + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-file :thumbnails id] data) + (cond-> (nil? (get-in state [::update-thumbnail-lock id])) + (assoc-in [::update-thumbnail-lock id] lock)))) + + ptk/WatchEvent + (watch [_ state stream] + (when (= lock (get-in state [::update-thumbnail-lock id])) + (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize))) + params {:file-id (:current-file-id state) + :object-id id}] + ;; Sends the first event and debounce the rest. Will only make one update once + ;; the 2 second debounce is finished + (rx/merge + (->> stream + (rx/filter (ptk/type? ::update-thumbnail)) + (rx/map deref) + (rx/filter #(= id (:id %))) + (rx/debounce 2000) + (rx/take 1) + (rx/map :data) + (rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %))) + (rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock id]))) + (rx/take-until stopper)) + + (->> (rx/of (update-thumbnail id data)) + (rx/observe-on :async))))))))) + +(defn remove-thumbnail + [id] + (ptk/reify ::remove-thumbnail + ptk/UpdateEvent + (update [_ state] + (-> state (d/dissoc-in [:workspace-file :thumbnails id]))) + + ptk/WatchEvent + (watch [_ state _] + (let [params {:file-id (:current-file-id state) + :object-id id + :data nil}] + (->> (rp/mutation! :upsert-file-object-thumbnail params) + (rx/ignore)))))) + +(defn- extract-frame-changes + "Process a changes set in a commit to extract the frames that are changing" + [[event [old-objects new-objects]]] + (let [changes (-> event deref :changes) + + extract-ids + (fn [{type :type :as change}] + (case type + :add-obj [(:id change)] + :mod-obj [(:id change)] + :del-obj [(:id change)] + :reg-objects (:shapes change) + :mov-objects (:shapes change) + [])) + + get-frame-id + (fn [id] + (let [shape (or (get new-objects id) + (get old-objects id))] + (or (and (cph/frame-shape? shape) id) (:frame-id shape)))) + + ;; Extracts the frames and then removes nils and the root frame + xform (comp (mapcat extract-ids) + (map get-frame-id) + (remove nil?) + (filter #(not= uuid/zero %)) + (filter #(contains? new-objects %)))] + + (into #{} xform changes))) + +(defn thumbnail-change? + "Checks if a event is only updating thumbnails to ignore in the thumbnail generation process" + [event] + (let [changes (-> event deref :changes) + + is-thumbnail-op? + (fn [{type :type attr :attr}] + (and (= type :set) + (= attr :thumbnail))) + + is-thumbnail-change? + (fn [change] + (and (= (:type change) :mod-obj) + (->> change :operations (every? is-thumbnail-op?))))] + + (->> changes (every? is-thumbnail-change?)))) + +(defn watch-state-changes + "Watch the state for changes inside frames. If a change is detected will force a rendering + of the frame data so the thumbnail can be updated." + [] + (ptk/reify ::watch-state-changes + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper (->> stream + (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) + (= ::watch-state-changes (ptk/type %))))) + + objects-stream (->> (rx/concat + (rx/of nil) + (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})) + ;; We need to keep the old-objects so we can check the frame for the + ;; deleted objects + (rx/buffer 2 1)) + + frame-changes (->> stream + (rx/filter dch/commit-changes?) + + ;; Async so we wait for additional side-effects of commit-changes + (rx/observe-on :async) + (rx/filter (complement thumbnail-change?)) + (rx/with-latest-from objects-stream) + (rx/map extract-frame-changes) + (rx/share))] + + (->> frame-changes + (rx/flat-map + (fn [ids] + (->> (rx/from ids) + (rx/map #(ptk/data-event ::force-render %))))) + (rx/take-until stopper)))))) + +(defn duplicate-thumbnail + [old-id new-id] + (ptk/reify ::duplicate-thumbnail + ptk/UpdateEvent + (update [_ state] + (let [old-shape-thumbnail (get-in state [:workspace-file :thumbnails old-id])] + (-> state (assoc-in [:workspace-file :thumbnails new-id] old-shape-thumbnail)))))) + + diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index accbc9e13..961f6af36 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -248,11 +248,28 @@ (dm/get-in data [:pages-index page-id]))) st/state)) +(defn workspace-page-objects-by-id + [page-id] + (l/derived #(wsh/lookup-page-objects % page-id) st/state =)) + (def workspace-page-objects (l/derived wsh/lookup-page-objects st/state =)) -(def workspace-modifiers - (l/derived :workspace-modifiers st/state)) +(defn object-by-id + [id] + (l/derived #(get % id) workspace-page-objects)) + +(defn objects-by-id + [ids] + (l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =)) + +(defn children-objects + [id] + (l/derived + (fn [objects] + (let [children-ids (get-in objects [id :shapes])] + (into [] (keep (d/getf objects)) children-ids))) + workspace-page-objects =)) (def workspace-page-options (l/derived :options workspace-page)) @@ -266,13 +283,35 @@ (def workspace-editor-state (l/derived :workspace-editor-state st/state)) -(defn object-by-id - [id] - (l/derived #(get % id) workspace-page-objects)) +(def workspace-modifiers + (l/derived :workspace-modifiers st/state)) -(defn objects-by-id +(defn workspace-modifiers-by-id [ids] - (l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects)) + (l/derived #(select-keys % ids) workspace-modifiers)) + + +(def workspace-modifiers-with-objects + (l/derived + (fn [state] + {:modifiers (:workspace-modifiers state) + :objects (wsh/lookup-page-objects state)}) + st/state + (fn [a b] + (and (= (:modifiers a) (:modifiers b)) + (identical? (:objects a) (:objects b)))))) + +(defn workspace-modifiers-by-frame-id + [frame-id] + (l/derived + (fn [{:keys [modifiers objects]}] + (let [keys (->> modifiers + (keys) + (filter #(or (= frame-id %) + (= frame-id (get-in objects [% :frame-id])))))] + (select-keys modifiers keys))) + workspace-modifiers-with-objects + =)) (defn- set-content-modifiers [state] (fn [id shape] @@ -355,3 +394,16 @@ (l/derived (fn [state] (dm/get-in state [:viewer-local :fullscreen?])) st/state)) + +(def thumbnail-data + (l/derived #(dm/get-in % [:workspace-file :thumbnails] {}) st/state)) + +(defn thumbnail-frame-data + [frame-id] + (l/derived #(get % frame-id) thumbnail-data)) + +(def workspace-text-modifier + (l/derived :workspace-text-modifier st/state)) + +(defn workspace-text-modifier-by-id [id] + (l/derived #(get % id) workspace-text-modifier)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 9b61b96e9..b170a2c08 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -243,8 +243,9 @@ (let [shapes (->> shapes (remove cph/frame-shape?) - (mapcat #(cph/get-children-with-self objects (:id %))))] - [:& ff/fontfaces-style {:shapes shapes}]) + (mapcat #(cph/get-children-with-self objects (:id %)))) + fonts (ff/shapes->fonts shapes)] + [:& ff/fontfaces-style {:fonts fonts}]) (for [item shapes] (let [frame? (= (:type item) :frame)] @@ -362,19 +363,15 @@ (mf/defc object-svg {::mf/wrap [mf/memo]} - [{:keys [objects object zoom render-texts? render-embed?] - :or {zoom 1 render-embed? false} + [{:keys [objects object-id render-texts? render-embed?] + :or {render-embed? false} :as props}] - (let [object (cond-> object + (let [object (get objects object-id) + object (cond-> object (:hide-fill-on-export object) (assoc :fills [])) - obj-id (:id object) - x (* (:x object) zoom) - y (* (:y object) zoom) - width (* (:width object) zoom) - height (* (:height object) zoom) - + {:keys [x y width height]} (get-object-bounds objects object-id) vbox (dm/str x " " y " " width " " height) frame-wrapper @@ -393,7 +390,7 @@ render-texts? (and render-texts? (d/seek (comp nil? :position-data) text-shapes))] [:& (mf/provider embed/context) {:value render-embed?} - [:svg {:id (dm/str "screenshot-" obj-id) + [:svg {:id (dm/str "screenshot-" object-id) :view-box vbox :width width :height height @@ -405,8 +402,8 @@ :style {:-webkit-print-color-adjust :exact} :fill "none"} - (let [shapes (cph/get-children objects obj-id)] - [:& ff/fontfaces-style {:shapes shapes}]) + (let [fonts (ff/frame->fonts object-id objects)] + [:& ff/fontfaces-style {:fonts fonts}]) (case (:type object) :frame [:& frame-wrapper {:shape object :view-box vbox}] diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index bcf746f50..93ad15877 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.dashboard (:require + [app.common.colors :as clr] [app.common.spec :as us] [app.main.data.dashboard :as dd] [app.main.data.dashboard.shortcuts :as sc] @@ -22,6 +23,7 @@ [app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page]] [app.main.ui.hooks :as hooks] + [app.util.dom :as dom] [app.util.keyboard :as kbd] [goog.events :as events] [rumext.alpha :as mf]) @@ -103,6 +105,7 @@ (mf/use-effect (fn [] + (dom/set-html-theme-color clr/white "light") (let [events [(events/listen goog/global EventType.KEYDOWN (fn [event] (when (kbd/enter? event) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index cc30870c9..d9c2b72ba 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -213,6 +213,15 @@ (mf/set-ref-val! ref value))) (mf/ref-val ref))) +(defn use-update-var + [value] + (let [ref (mf/use-var value)] + (mf/use-effect + (mf/deps value) + (fn [] + (reset! ref value))) + ref)) + (defn use-equal-memo [val] (let [ref (mf/use-ref nil)] @@ -248,3 +257,5 @@ (mf/deps focus objects) #(cp/focus-objects objects focus))] objects))) + + diff --git a/frontend/src/app/main/ui/hooks/mutable_observer.cljs b/frontend/src/app/main/ui/hooks/mutable_observer.cljs index b3dabaf45..239deea2e 100644 --- a/frontend/src/app/main/ui/hooks/mutable_observer.cljs +++ b/frontend/src/app/main/ui/hooks/mutable_observer.cljs @@ -46,7 +46,10 @@ :characterData true} mutation-obs (js/MutationObserver. on-mutation)] (mf/set-ref-val! prev-obs-ref mutation-obs) - (.observe mutation-obs node options))))))] + (.observe mutation-obs node options)))) + + ;; Return node so it's more composable + node))] (mf/with-effect (fn [] diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 2f3181c0e..f879e2345 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -220,8 +220,9 @@ (mf/defc selection-guides [{:keys [bounds selrect zoom]}] [:g.selection-guides - (for [[x1 y1 x2 y2] (calculate-guides bounds selrect)] - [:line {:x1 x1 + (for [[idx [x1 y1 x2 y2]] (d/enumerate (calculate-guides bounds selrect))] + [:line {:key (dm/str "guide-" idx) + :x1 x1 :y1 y1 :x2 x2 :y2 y2 diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 18bd62c00..fa69ae097 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -382,6 +382,14 @@ (some? style) (obj/set! "style" style))) + (and (= :path (:type shape)) (empty? (:fills shape))) + (let [style + (-> (obj/get props "style") + (obj/clone) + (obj/set! "fill" "none"))] + (-> props + (obj/set! "style" style))) + :else props))) @@ -405,7 +413,6 @@ shape (obj/get props "shape") elem-name (obj/get child "type") render-id (mf/use-ctx muc/render-ctx)] - [:g {:id (dm/fmt "fills-%" (:id shape))} [:> elem-name (build-fill-props shape child render-id)]])) @@ -416,11 +423,17 @@ shape (obj/get props "shape") elem-name (obj/get child "type") render-id (mf/use-ctx muc/render-ctx) + stroke-id (dm/fmt "strokes-%" (:id shape)) stroke-props (-> (obj/new) - (obj/set! "id" (dm/fmt "strokes-%" (:id shape))) + (obj/set! "id" stroke-id) (cond-> - (and (and (:blur shape) (-> shape :blur :hidden not)) (not (cph/frame-shape? shape))) - (obj/set! "filter" (dm/fmt "url(#filter_blur_%)" render-id))))] + ;; There is a blur + (and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not)) + (obj/set! "filter" (dm/fmt "url(#filter_blur_%)" render-id)) + + ;; There are any shadows and no fills + (and (empty? (:fills shape)) (not (cph/frame-shape? shape)) (seq (->> (:shadow shape) (remove :hidden)))) + (obj/set! "filter" (dm/fmt "url(#filter_%)" render-id))))] [:* (when (d/not-empty? (:strokes shape)) @@ -428,18 +441,14 @@ (for [[index value] (-> (d/enumerate (:strokes shape)) reverse)] (let [props (build-stroke-props index child value render-id) shape (assoc value :points (:points shape))] - [:& shape-custom-stroke {:shape shape :index index} + [:& shape-custom-stroke {:shape shape :index index :key (dm/str index "-" stroke-id)} [:> elem-name props]]))])])) (mf/defc shape-custom-strokes {::mf/wrap-props false} [props] - (let [child (obj/get props "children") - shape (obj/get props "shape")] - + (let [children (obj/get props "children") + shape (obj/get props "shape")] [:* - [:& shape-fills {:shape shape} - child] - - [:& shape-strokes {:shape shape} - child]])) + [:& shape-fills {:shape shape} children] + [:& shape-strokes {:shape shape} children]])) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index ae3bef8a8..df285209e 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.fills (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.config :as cfg] [app.main.ui.shapes.attrs :as attrs] @@ -55,10 +56,11 @@ (obj/set! "patternTransform" transform))] (for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))] - [:* + [:* {:key (dm/str shape-index)} (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] (when (some? (:fill-color-gradient value)) - (let [props #js {:id (str "fill-color-gradient_" render-id "_" fill-index) + (let [props #js {:id (dm/str "fill-color-gradient_" render-id "_" fill-index) + :key (dm/str fill-index) :gradient (:fill-color-gradient value) :shape shape}] (case (d/name (:type (:fill-color-gradient value))) @@ -66,12 +68,13 @@ "radial" [:> grad/radial-gradient props])))) - (let [fill-id (str "fill-" shape-index "-" render-id)] + (let [fill-id (dm/str "fill-" shape-index "-" render-id)] [:> :pattern (-> (obj/clone pattern-attrs) (obj/set! "id" fill-id)) [:g (for [[fill-index value] (-> (d/enumerate (:fills shape [])) reverse)] [:> :rect (-> (attrs/extract-fill-attrs value render-id fill-index) + (obj/set! "key" (dm/str fill-index)) (obj/set! "width" width) (obj/set! "height" height))]) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 1e2d85df5..0d371ce49 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -58,37 +58,38 @@ (defn frame-shape [shape-wrapper] (mf/fnc frame-shape - {::mf/wrap-props false} - [props] - (let [childs (unchecked-get props "childs") - shape (unchecked-get props "shape") - {:keys [x y width height]} shape - transform (gsh/transform-matrix shape) + {::mf/wrap-props false} + [props] + (let [childs (unchecked-get props "childs") + shape (unchecked-get props "shape") + {:keys [x y width height]} shape - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :transform transform - :width width - :height height - :className "frame-background"})) - path? (some? (.-d props)) - render-id (mf/use-ctx muc/render-ctx)] + transform (gsh/transform-matrix shape) - [:* - [:g {:clip-path (frame-clip-url shape render-id)} - [:* - [:& shape-fills {:shape shape} - (if path? - [:> :path props] - [:> :rect props])] + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x x + :y y + :transform (str transform) + :width width + :height height + :className "frame-background"})) + path? (some? (.-d props)) + render-id (mf/use-ctx muc/render-ctx)] - (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}]) - [:& shape-strokes {:shape shape} - (if path? - [:> :path props] - [:> :rect props])]]]]))) + [:* + [:g {:clip-path (frame-clip-url shape render-id)} + [:* + [:& shape-fills {:shape shape} + (if path? + [:> :path props] + [:> :rect props])] + + (for [item childs] + [:& shape-wrapper {:shape item + :key (dm/str (:id item))}]) + [:& shape-strokes {:shape shape} + (if path? + [:> :path props] + [:> :rect props])]]]]))) diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index 5e3fd46af..1b192f76d 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -50,46 +50,38 @@ (let [mask (unchecked-get props "mask") render-id (mf/use-ctx muc/render-ctx) svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) - mask (cond-> mask svg-text? set-white-fill) - ;; This factory is generic, it's used for viewer, workspace and handoff. ;; These props are generated in viewer wrappers only, in the rest of the ;; cases these props will be nil, not affecting the code. fixed? (unchecked-get props "fixed?") delta (unchecked-get props "delta") - mask-for-bb (-> (gsh/transform-shape mask) (cond-> fixed? (gsh/move delta))) mask-bb (cond svg-text? (gst/position-data-points mask-for-bb) :else (:points mask-for-bb))] - [:* - [:g {:opacity 0} - [:g {:id (str "shape-" (mask-id render-id mask))} - [:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]] + [:defs + [:filter {:id (filter-id render-id mask)} + [:feFlood {:flood-color "white" + :result "FloodResult"}] + [:feComposite {:in "FloodResult" + :in2 "SourceGraphic" + :operator "in" + :result "comp"}]] + ;; Clip path is necessary so the elements inside the mask won't affect + ;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility) + ;; we cannot use clips instead of mask because clips can only be simple shapes + [:clipPath {:class "mask-clip-path" + :id (clip-id render-id mask)} + [:polyline {:points (->> mask-bb + (map #(str (:x %) "," (:y %))) + (str/join " "))}]] - [:defs - [:filter {:id (filter-id render-id mask)} - [:feFlood {:flood-color "white" - :result "FloodResult"}] - [:feComposite {:in "FloodResult" - :in2 "SourceGraphic" - :operator "in" - :result "comp"}]] - ;; Clip path is necessary so the elements inside the mask won't affect - ;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility) - ;; we cannot use clips instead of mask because clips can only be simple shapes - [:clipPath {:class "mask-clip-path" - :id (clip-id render-id mask)} - [:polyline {:points (->> mask-bb - (map #(str (:x %) "," (:y %))) - (str/join " "))}]] - - [:mask {:class "mask-shape" - :id (mask-id render-id mask)} - ;; SVG texts are broken in Firefox with the filter. When the masking shapes is a text - ;; we use the `set-white-fill` instead of using the filter - [:g {:filter (when-not svg-text? (filter-url render-id mask))} - [:use {:href (str "#shape-" (mask-id render-id mask))}]]]]]))) + ;; When te shape is a text we pass to the shape the info and disable the filter. + ;; There is a bug in Firefox with filters and texts. We change the text to white at shape level + [:mask {:class "mask-shape" + :id (mask-id render-id mask)} + [:g {:filter (when-not svg-text? (filter-url render-id mask))} + [:& shape-wrapper {:shape (-> mask (dissoc :shadow :blur) (assoc :is-mask? true))}]]]]))) diff --git a/frontend/src/app/main/ui/shapes/svg_defs.cljs b/frontend/src/app/main/ui/shapes/svg_defs.cljs index a3662e49c..e0d7bbc18 100644 --- a/frontend/src/app/main/ui/shapes/svg_defs.cljs +++ b/frontend/src/app/main/ui/shapes/svg_defs.cljs @@ -6,8 +6,10 @@ (ns app.main.ui.shapes.svg-defs (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] + [app.common.geom.shapes :as gsh] [app.main.ui.shapes.filters :as f] [app.util.object :as obj] [app.util.svg :as usvg] @@ -60,7 +62,8 @@ transform-gradient? (add-matrix :gradientTransform transform) transform-pattern? (add-matrix :patternTransform transform) transform-clippath? (add-matrix :transform transform) - (or transform-filter? transform-mask?) (merge bounds))) + (or transform-filter? + transform-mask?) (merge bounds))) [wrapper wrapper-props] (if (= tag :mask) ["g" #js {:className "svg-mask-wrapper" @@ -75,6 +78,16 @@ :transform transform :bounds bounds}])]]))) +(defn svg-def-bounds [svg-def shape transform] + (let [{:keys [tag]} svg-def] + (if (or (= tag :mask) (contains? usvg/filter-tags tag)) + (-> (gsh/make-rect (d/parse-double (get-in svg-def [:attrs :x])) + (d/parse-double (get-in svg-def [:attrs :y])) + (d/parse-double (get-in svg-def [:attrs :width])) + (d/parse-double (get-in svg-def [:attrs :height]))) + (gsh/transform-rect transform)) + (f/get-filters-bounds shape)))) + (mf/defc svg-defs [{:keys [shape render-id]}] (let [svg-defs (:svg-defs shape) @@ -101,5 +114,4 @@ :node svg-def :prefix-id prefix-id :transform transform - :bounds (f/get-filters-bounds shape)}])))) - + :bounds (svg-def-bounds svg-def shape transform)}])))) diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs index 68c26f713..ca950985e 100644 --- a/frontend/src/app/main/ui/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -34,9 +34,9 @@ (obj/set! "style" style)))) (defn translate-shape [attrs shape] - (let [transform (str (usvg/svg-transform-matrix shape) - " " - (:transform attrs ""))] + (let [transform (dm/str (usvg/svg-transform-matrix shape) + " " + (:transform attrs ""))] (cond-> attrs (and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag))) (assoc :transform transform)))) @@ -47,7 +47,6 @@ (let [shape (unchecked-get props "shape") children (unchecked-get props "children") - {:keys [x y width height]} shape {:keys [attrs] :as content} (:content shape) @@ -61,7 +60,7 @@ (obj/set! "preserveAspectRatio" "none"))] [:& (mf/provider svg-ids-ctx) {:value ids-mapping} - [:g.svg-raw {:transform (gsh/transform-matrix shape)} + [:g.svg-raw {:transform (dm/str (gsh/transform-matrix shape))} [:> "svg" attrs children]]])) (mf/defc svg-element diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 86ef063ed..1e6a259d1 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -20,9 +20,13 @@ (mf/defc render-text {::mf/wrap-props false} [props] - (let [node (obj/get props "node") - text (:text node) - style (sts/generate-text-styles node)] + (let [node (obj/get props "node") + parent (obj/get props "parent") + shape (obj/get props "shape") + text (:text node) + style (if (= text "") + (sts/generate-text-styles shape parent) + (sts/generate-text-styles shape node))] [:span.text-node {:style style} (if (= text "") "\u00A0" text)])) @@ -60,7 +64,7 @@ (mf/defc render-node {::mf/wrap-props false} [props] - (let [{:keys [type text children] :as node} (obj/get props "node")] + (let [{:keys [type text children]} (obj/get props "node")] (if (string? text) [:> render-text props] (let [component (case type diff --git a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs index 9e2730138..f2a903948 100644 --- a/frontend/src/app/main/ui/shapes/text/fontfaces.cljs +++ b/frontend/src/app/main/ui/shapes/text/fontfaces.cljs @@ -73,16 +73,25 @@ (when (d/not-empty? style) [:style style]))) +(defn frame->fonts + [frame objects] + (->> (cph/get-children objects (:id frame)) + (filter cph/text-shape?) + (map (comp fonts/get-content-fonts :content)) + (reduce set/union #{}))) + +(defn shapes->fonts + [shapes] + (->> shapes + (filter cph/text-shape?) + (map (comp fonts/get-content-fonts :content)) + (reduce set/union #{}))) + (mf/defc fontfaces-style {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]} + ::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]} [props] (let [;; Retrieve the fonts ids used by the text shapes - fonts (->> (obj/get props "shapes") - (filterv cph/text-shape?) - (mapv (comp fonts/get-content-fonts :content)) - (reduce set/union #{}) - (hooks/use-equal-memo))] - + fonts (obj/get props "fonts")] (when (d/not-empty? fonts) [:> fontfaces-style-render {:fonts fonts}]))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index bfa35f81f..cd2a21c5b 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -15,13 +15,13 @@ [cuerdas.core :as str])) (defn generate-root-styles - [shape node] + [{:keys [width height]} node] (let [valign (:vertical-align node "top") - {:keys [width height]} shape base #js {:height height :width width :fontFamily "sourcesanspro" - :display "flex"}] + :display "flex" + :whiteSpace "pre-wrap"}] (cond-> base (= valign "top") (obj/set! "alignItems" "flex-start") (= valign "center") (obj/set! "alignItems" "center") @@ -45,24 +45,21 @@ :verticalAlign "top"})) (defn generate-paragraph-styles - [shape data] + [_shape data] (let [line-height (:line-height data 1.2) text-align (:text-align data "start") - grow-type (:grow-type shape) - base #js {:fontSize (str (:font-size data (:font-size txt/default-text-attrs)) "px") :lineHeight (:line-height data (:line-height txt/default-text-attrs)) :margin "inherit"}] (cond-> base (some? line-height) (obj/set! "lineHeight" line-height) - (some? text-align) (obj/set! "textAlign" text-align) - (= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))) + (some? text-align) (obj/set! "textAlign" text-align)))) (defn generate-text-styles - ([data] - (generate-text-styles data nil)) + ([shape data] + (generate-text-styles shape data nil)) - ([data {:keys [show-text?] :or {show-text? true}}] + ([{:keys [grow-type] :as shape} data {:keys [show-text?] :or {show-text? true}}] (let [letter-spacing (:letter-spacing data 0) text-decoration (:text-decoration data) text-transform (:text-transform data) @@ -83,7 +80,7 @@ base #js {:textDecoration text-decoration :textTransform text-transform - :lineHeight (or line-height "inherit") + :lineHeight (or line-height "1.2") :color (if show-text? text-color "transparent") :caretColor (or text-color "black") :overflowWrap "initial"} @@ -101,33 +98,35 @@ (nil? (:fills data)) [{:fill-color "#000000" :fill-opacity 1}]) - base (cond-> base - (some? fills) - (obj/set! "--fills" (transit/encode-str fills)))] - (when (and (string? letter-spacing) - (pos? (alength letter-spacing))) - (obj/set! base "letterSpacing" (str letter-spacing "px"))) + font (when (and (string? font-id) (pos? (alength font-id))) + (get fontsdb font-id)) - (when (and (string? font-size) - (pos? (alength font-size))) - (obj/set! base "fontSize" (str font-size "px"))) + [font-family font-style font-weight] + (when (some? font) + (fonts/ensure-loaded! font-id) + (let [font-variant (d/seek #(= font-variant-id (:id %)) (:variants font))] + [(str/quote (or (:family font) (:font-family data))) + (or (:style font-variant) (:font-style data)) + (or (:weight font-variant) (:font-weight data))]))] - (when (and (string? font-id) - (pos? (alength font-id))) - (fonts/ensure-loaded! font-id) - (let [font (get fontsdb font-id) - font-family (str/quote - (or (:family font) - (:font-family data))) - font-variant (d/seek #(= font-variant-id (:id %)) - (:variants font)) - font-style (or (:style font-variant) - (:font-style data)) - font-weight (or (:weight font-variant) - (:font-weight data))] - (obj/set! base "fontFamily" font-family) - (obj/set! base "fontStyle" font-style) - (obj/set! base "fontWeight" font-weight))) + (cond-> base + (some? fills) + (obj/set! "--fills" (transit/encode-str fills)) - base))) + (and (string? letter-spacing) (pos? (alength letter-spacing))) + (obj/set! "letterSpacing" (str letter-spacing "px")) + + (and (string? font-size) (pos? (alength font-size))) + (obj/set! "fontSize" (str font-size "px")) + + (some? font) + (-> (obj/set! "fontFamily" font-family) + (obj/set! "fontStyle" font-style) + (obj/set! "fontWeight" font-weight)) + + (= grow-type :auto-width) + (obj/set! "whiteSpace" "pre") + + (not= grow-type :auto-width) + (obj/set! "whiteSpace" "pre-wrap"))))) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index c711caf2d..03cfd7235 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.shapes.text.svg-text (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.config :as cfg] [app.main.ui.context :as muc] @@ -18,13 +19,27 @@ (def fill-attrs [:fill-color :fill-color-gradient :fill-opacity]) +(defn set-white-fill + [shape] + (let [update-color + (fn [data] + (-> data + (dissoc :fill-color :fill-opacity :fill-color-gradient) + (assoc :fills [{:fill-color "#FFFFFF" :fill-opacity 1}])))] + (-> shape + (d/update-when :position-data #(mapv update-color %)) + (assoc :stroke-color "#FFFFFF" :stroke-opacity 1)))) + (mf/defc text-shape {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] (let [render-id (mf/use-ctx muc/render-ctx) - {:keys [x y width height position-data] :as shape} (obj/get props "shape") + shape (obj/get props "shape") + shape (cond-> shape (:is-mask? shape) set-white-fill) + + {:keys [x y width height position-data]} shape transform (str (gsh/transform-matrix shape)) @@ -45,6 +60,7 @@ (for [[index data] (d/enumerate position-data)] (when (some? (:fill-color-gradient data)) [:& grad/gradient {:id (str "fill-color-gradient_" (get-gradient-id index)) + :key index :attr :fill-color-gradient :shape data}]))]) @@ -56,7 +72,8 @@ alignment-bl (when (cfg/check-browser? :safari) "text-before-edge") dominant-bl (when-not (cfg/check-browser? :safari) "ideographic") - props (-> #js {:x (:x data) + props (-> #js {:key (dm/str "text-" (:id shape) "-" index) + :x (:x data) :y y :alignmentBaseline alignment-bl :dominantBaseline dominant-bl @@ -71,5 +88,5 @@ (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}) shape (assoc shape :fills (:fills data))] - [:& shape-custom-strokes {:shape shape} + [:& shape-custom-strokes {:shape shape :key index} [:> :text props (:text data)]]))]])) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index dc28de25d..c148c3db1 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.viewer (:require + [app.common.colors :as clr] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.geom.point :as gpt] @@ -149,6 +150,7 @@ (mf/use-effect (fn [] + (dom/set-html-theme-color clr/gray-50 "dark") (let [key1 (events/listen js/window "click" on-click) key2 (events/listen (mf/ref-val viewer-section-ref) "scroll" on-scroll)] (fn [] diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 92818e611..17b1fce61 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.workspace (:require + [app.common.colors :as clr] [app.common.data.macros :as dm] [app.main.data.messages :as msg] [app.main.data.workspace :as dw] @@ -130,8 +131,9 @@ (st/emit! ::dwp/force-persist (dw/finalize-file project-id file-id)))) - ;; Close any non-modal dialog that may be still open + ;; Set html theme color and close any non-modal dialog that may be still open (mf/with-effect + (dom/set-html-theme-color clr/gray-50 "dark") (st/emit! msg/hide)) ;; Set properly the page title diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index adff4aa4c..78cb6ab8b 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -44,7 +44,14 @@ [props] (let [objects (obj/get props "objects") active-frames (obj/get props "active-frames") - shapes (cph/get-immediate-children objects)] + shapes (cph/get-immediate-children objects) + + ;; We group the objects together per frame-id so if an object of a different + ;; frame changes won't affect the rendering frame + frame-objects + (mf/use-memo + (mf/deps objects) + #(cph/objects-by-frame objects))] [:* ;; Render font faces only for shapes that are part of the root ;; frame but don't belongs to any other frame. @@ -57,7 +64,7 @@ (if (cph/frame-shape? item) [:& frame-wrapper {:shape item :key (:id item) - :objects objects + :objects (get frame-objects (:id item)) :thumbnail? (not (get active-frames (:id item) false))}] [:& shape-wrapper {:shape item @@ -67,9 +74,8 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] - (let [shape (obj/get props "shape") + (let [shape (obj/get props "shape") opts #js {:shape shape}] - (when (and (some? shape) (not (:hidden shape))) [:* (case (:type shape) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 7a7f2fa92..b2b9a8c97 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,125 +6,117 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.common.colors :as cc] [app.common.data :as d] - [app.common.pages.helpers :as cph] + [app.common.data.macros :as dm] + [app.main.data.workspace.thumbnails :as dwt] + [app.main.refs :as refs] [app.main.ui.hooks :as hooks] + [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] [app.main.ui.shapes.text.fontfaces :as ff] - [app.util.object :as obj] - [app.util.timers :as ts] + [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] + [app.main.ui.workspace.shapes.frame.node-store :as fns] + [app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr] [beicon.core :as rx] [rumext.alpha :as mf])) -(defn check-frame-props - "Checks for changes in the props of a frame" - [new-props old-props] - (let [new-shape (unchecked-get new-props "shape") - old-shape (unchecked-get old-props "shape") - - new-thumbnail? (unchecked-get new-props "thumbnail?") - old-thumbnail? (unchecked-get old-props "thumbnail?") - - new-objects (unchecked-get new-props "objects") - old-objects (unchecked-get old-props "objects") - - new-children (->> new-shape :shapes (mapv #(get new-objects %))) - old-children (->> old-shape :shapes (mapv #(get old-objects %)))] - (and (= new-shape old-shape) - (= new-thumbnail? old-thumbnail?) - (= new-children old-children)))) - -(mf/defc frame-placeholder - {::mf/wrap-props false} - [props] - (let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")] - (if (some? (:thumbnail shape)) - [:& frame/frame-thumbnail {:shape shape}] - [:rect.frame-thumbnail {:x x :y y :width width :height height :style {:fill (or fill-color cc/white)}}]))) - -(defn custom-deferred - [component] - (mf/fnc deferred - {::mf/wrap-props false} - [props] - (let [shape (-> (obj/get props "shape") - (select-keys [:x :y :width :height]) - (hooks/use-equal-memo)) - - tmp (mf/useState false) - ^boolean render? (aget tmp 0) - ^js set-render (aget tmp 1) - prev-shape-ref (mf/use-ref shape)] - - (mf/use-effect - (mf/deps shape) - (fn [] - (mf/set-ref-val! prev-shape-ref shape) - (set-render false))) - - (mf/use-effect - (mf/deps render? shape) - (fn [] - (when-not render? - (let [sem (ts/schedule-on-idle #(set-render true))] - #(rx/dispose! sem))))) - - (if (and render? (= shape (mf/ref-val prev-shape-ref))) - (mf/create-element component props) - (mf/create-element frame-placeholder props))))) - -;; Draw the frame proper as a deferred component -(defn deferred-frame-shape-factory +(defn frame-shape-factory [shape-wrapper] (let [frame-shape (frame/frame-shape shape-wrapper)] - (mf/fnc defered-frame-wrapper - {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "childs"])) - custom-deferred]} - [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs")] - [:& frame-shape {:shape shape - :childs childs}])))) + (mf/fnc frame-shape-inner + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "fonts"]))] + ::mf/wrap-props false + ::mf/forward-ref true} + [props ref] + + (let [shape (unchecked-get props "shape") + fonts (unchecked-get props "fonts") + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) + childs (mf/deref childs-ref)] + + [:& (mf/provider embed/context) {:value true} + [:& shape-container {:shape shape :ref ref} + [:& ff/fontfaces-style {:fonts fonts}] + [:& frame-shape {:shape shape :childs childs} ]]])))) + +(defn check-props + [new-props old-props] + (and + (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) + (= (unchecked-get new-props "shape") (unchecked-get old-props "shape")) + (= (unchecked-get new-props "objects") (unchecked-get old-props "objects")))) (defn frame-wrapper-factory [shape-wrapper] - (let [deferred-frame-shape (deferred-frame-shape-factory shape-wrapper)] + + (let [frame-shape (frame-shape-factory shape-wrapper)] (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % check-frame-props)] + {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] - (when-let [shape (unchecked-get props "shape")] - (let [objects (unchecked-get props "objects") - thumbnail? (unchecked-get props "thumbnail?") + (let [shape (unchecked-get props "shape") + thumbnail? (unchecked-get props "thumbnail?") + objects (unchecked-get props "objects") - children - (-> (mapv (d/getf objects) (:shapes shape)) - (hooks/use-equal-memo)) + fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects)) + fonts (-> fonts (hooks/use-equal-memo)) - all-children - (-> (cph/get-children objects (:id shape)) - (hooks/use-equal-memo)) + force-render (mf/use-state false) - all-svg-text? - (mf/use-memo - (mf/deps all-children) - (fn [] - (->> all-children - (filter #(and (= :text (:type %)) (not (:hidden %)))) - (every? #(some? (:position-data %)))))) + ;; Thumbnail data + frame-id (:id shape) + thumbnail-data-ref (mf/use-memo (mf/deps frame-id) #(refs/thumbnail-frame-data frame-id)) + thumbnail-data (mf/deref thumbnail-data-ref) + thumbnail? (and thumbnail? (or (some? (:thumbnail shape)) (some? thumbnail-data))) - show-thumbnail? (and thumbnail? (some? (:thumbnail shape)) all-svg-text?)] + ;; References to the current rendered node and the its parentn + node-ref (mf/use-var nil) - [:g.frame-wrapper {:display (when (:hidden shape) "none")} - [:> shape-container {:shape shape} - [:& ff/fontfaces-style {:shapes all-children}] - (if show-thumbnail? - [:& frame/frame-thumbnail {:shape shape}] - [:& deferred-frame-shape - {:shape shape - :childs children}])]]))))) + ;; when `true` we've called the mount for the frame + rendered? (mf/use-var false) + ;; Modifiers + modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers-ref) + + disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers])) + + [on-load-frame-dom thumb-renderer] + (ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable-thumbnail?) + + on-frame-load + (fns/use-node-store thumbnail? node-ref rendered?)] + + (fdm/use-dynamic-modifiers objects @node-ref modifiers) + + (mf/use-effect + (fn [] + ;; When a change in the data is received a "force-render" event is emited + ;; that will force the component to be mounted in memory + (let [sub + (->> (dwt/force-render-stream (:id shape)) + (rx/take-while #(not @rendered?)) + (rx/subs #(reset! force-render true)))] + #(when sub + (rx/dispose! sub))))) + + (mf/use-effect + (mf/deps shape fonts thumbnail? on-load-frame-dom @force-render) + (fn [] + (when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render)) + (mf/mount + (mf/element frame-shape + #js {:ref on-load-frame-dom :shape shape :fonts fonts}) + + @node-ref) + (when (not @rendered?) (reset! rendered? true))))) + + [:g.frame-container {:key "frame-container" :ref on-frame-load} + thumb-renderer + + [:g.frame-thumbnail + [:> frame/frame-thumbnail {:shape (cond-> shape + (some? thumbnail-data) + (assoc :thumbnail thumbnail-data))}]]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs new file mode 100644 index 000000000..2fde51491 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -0,0 +1,54 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.dynamic-modifiers + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.main.ui.workspace.viewport.utils :as utils] + [rumext.alpha :as mf])) + +(defn use-dynamic-modifiers + [objects node modifiers] + + (let [transforms + (mf/use-memo + (mf/deps modifiers) + (fn [] + (when (some? modifiers) + (d/mapm (fn [id {modifiers :modifiers}] + (let [center (gsh/center-shape (get objects id))] + (gsh/modifiers->transform center modifiers))) + modifiers)))) + + shapes + (mf/use-memo + (mf/deps transforms) + (fn [] + (->> (keys transforms) + (mapv (d/getf objects))))) + + prev-shapes (mf/use-var nil) + prev-modifiers (mf/use-var nil) + prev-transforms (mf/use-var nil)] + + (mf/use-layout-effect + (mf/deps transforms) + (fn [] + (when (and (nil? @prev-transforms) + (some? transforms)) + (utils/start-transform! node shapes)) + + (when (some? modifiers) + (utils/update-transform! node shapes transforms modifiers)) + + (when (and (some? @prev-modifiers) + (empty? modifiers)) + (utils/remove-transform! node @prev-shapes)) + + (reset! prev-modifiers modifiers) + (reset! prev-transforms transforms) + (reset! prev-shapes shapes))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs new file mode 100644 index 000000000..f5b73c68f --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.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/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.node-store + (:require + [app.util.dom :as dom] + [app.util.globals :as globals] + [rumext.alpha :as mf])) + +(defn use-node-store + "Hook responsible of storing the rendered DOM node in memory while not being used" + [thumbnail? node-ref rendered?] + + (let [;; when `true` the node is in memory + in-memory? (mf/use-var true) + + ;; State just for re-rendering + re-render (mf/use-state 0) + + parent-ref (mf/use-var nil) + + on-frame-load + (mf/use-callback + (fn [node] + (when (and (some? node) (nil? @node-ref)) + (let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g") + (dom/add-class! "frame-content"))] + ;;(.appendChild node content) + (reset! node-ref content) + (reset! parent-ref node) + (swap! re-render inc)))))] + + (mf/use-effect + (mf/deps thumbnail?) + (fn [] + (when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?) + (.removeChild @parent-ref @node-ref) + (reset! in-memory? true)) + + (when (and (some? @node-ref) @in-memory? (not thumbnail?)) + (.appendChild @parent-ref @node-ref) + (reset! in-memory? false)))) + + on-frame-load)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs new file mode 100644 index 000000000..aea3696a0 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs @@ -0,0 +1,121 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.frame.thumbnail-render + (:require + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.main.data.workspace :as dw] + [app.main.store :as st] + [app.main.ui.hooks :as hooks] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.timers :as ts] + [rumext.alpha :as mf])) + +(defn- draw-thumbnail-canvas + [canvas-node img-node] + (let [canvas-context (.getContext canvas-node "2d") + canvas-width (.-width canvas-node) + canvas-height (.-height canvas-node)] + (.clearRect canvas-context 0 0 canvas-width canvas-height) + (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) + (.toDataURL canvas-node "image/jpeg" 0.8))) + +(defn use-render-thumbnail + "Hook that will create the thumbnail thata" + [{:keys [id x y width height] :as shape} node-ref rendered? thumbnail? disable?] + + (let [frame-canvas-ref (mf/use-ref nil) + frame-image-ref (mf/use-ref nil) + + disable-ref? (mf/use-var disable?) + + fixed-width (mth/clamp (:width shape) 250 2000) + fixed-height (/ (* (:height shape) fixed-width) (:width shape)) + + image-url (mf/use-state nil) + observer-ref (mf/use-var nil) + + shape-ref (hooks/use-update-var shape) + + thumbnail-ref? (mf/use-var thumbnail?) + + on-image-load + (mf/use-callback + (fn [] + (let [canvas-node (mf/ref-val frame-canvas-ref) + img-node (mf/ref-val frame-image-ref)] + (ts/raf + #(let [thumb-data (draw-thumbnail-canvas canvas-node img-node)] + (st/emit! (dw/update-thumbnail id thumb-data)) + (reset! image-url nil)))))) + + on-change + (mf/use-callback + (fn [] + (when (and (some? @node-ref) (not @disable-ref?)) + (let [node @node-ref] + (ts/schedule-on-idle + #(let [frame-html (dom/node->xml node) + {:keys [x y width height]} @shape-ref + svg-node + (-> (dom/make-node "http://www.w3.org/2000/svg" "svg") + (dom/set-property! "version" "1.1") + (dom/set-property! "viewBox" (dm/str x " " y " " width " " height)) + (dom/set-property! "width" width) + (dom/set-property! "height" height) + (dom/set-property! "fill" "none") + (obj/set! "innerHTML" frame-html)) + + img-src (-> svg-node dom/node->xml dom/svg->data-uri)] + (reset! image-url img-src))))))) + + on-load-frame-dom + (mf/use-callback + (fn [node] + (when (and (some? node) (nil? @observer-ref)) + (on-change []) + (let [observer (js/MutationObserver. on-change)] + (.observe observer node #js {:childList true :attributes true :characterData true :subtree true}) + (reset! observer-ref observer)))))] + + (mf/use-effect + (mf/deps disable?) + (fn [] + (reset! disable-ref? disable?))) + + (mf/use-effect + (mf/deps thumbnail?) + (fn [] + (reset! thumbnail-ref? thumbnail?))) + + (mf/use-effect + (fn [] + #(when (and (some? @node-ref) @rendered?) + (mf/unmount @node-ref) + (reset! node-ref nil) + (reset! rendered? false) + (when (some? @observer-ref) + (.disconnect @observer-ref) + (reset! observer-ref nil))))) + + [on-load-frame-dom + (when (some? @image-url) + (mf/html + [:g.thumbnail-rendering {:opacity 0} + [:foreignObject {:x x :y y :width width :height height} + [:canvas {:ref frame-canvas-ref + :width fixed-width + :height fixed-height}]] + + [:image {:ref frame-image-ref + :x (:x shape) + :y (:y shape) + :xlinkHref @image-url + :width (:width shape) + :height (:height shape) + :on-load on-image-load}]]))])) diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index 18c1237ae..ff83b4228 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -30,9 +30,9 @@ {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) - childs (mf/deref childs-ref)] + (let [shape (unchecked-get props "shape") + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) + childs (mf/deref childs-ref)] [:> shape-container {:shape shape} [:& group-shape diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index a245d5bb2..dee228b39 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.path (:require [app.common.path.commands :as upc] + [app.main.data.workspace.path.helpers :as helpers] [app.main.refs :as refs] [app.main.ui.shapes.path :as path] [app.main.ui.shapes.shape :refer [shape-container]] @@ -21,7 +22,11 @@ content-modifiers (mf/deref content-modifiers-ref) editing-id (mf/deref refs/selected-edition) editing? (= editing-id (:id shape)) - shape (update shape :content upc/apply-content-modifiers content-modifiers)] + shape (update shape :content upc/apply-content-modifiers content-modifiers) + + [_ new-selrect] + (helpers/content->points+selrect shape (:content shape)) + shape (assoc shape :selrect new-selrect)] [:> shape-container {:shape shape :pointer-events (when editing? "none")} diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs index 7a3d7b1a4..2a09d2bfc 100644 --- a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -20,9 +20,9 @@ ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") - childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape))) + childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape))) childs (mf/deref childs-ref) - svg-tag (get-in shape [:content :tag])] + svg-tag (get-in shape [:content :tag])] (if (contains? usvg/svg-group-safe-tags svg-tag) [:> shape-container {:shape shape} [:& svg-raw-shape {:shape shape diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 85e635161..821fca536 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -6,228 +6,37 @@ (ns app.main.ui.workspace.shapes.text (:require - [app.common.attrs :as attrs] - [app.common.geom.matrix :as gmt] - [app.common.geom.shapes :as gsh] - [app.common.logging :as log] + [app.common.data :as d] [app.common.math :as mth] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.fo-text :as fo] - [app.main.ui.shapes.text.svg-text :as svg] - [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.svg :as usvg] - [app.util.text-editor :as ted] - [app.util.text-svg-position :as utp] - [app.util.timers :as timers] - [app.util.webapi :as wapi] - [beicon.core :as rx] + [app.main.ui.shapes.text :as text] [debug :refer [debug?]] - [okulary.core :as l] [rumext.alpha :as mf])) -;; Change this to :info :debug or :trace to debug this module -(log/set-level! :warn) - ;; --- Text Wrapper for workspace - -(mf/defc text-static-content - [{:keys [shape]}] - [:& fo/text-shape {:shape shape - :grow-type (:grow-type shape)}]) - -(defn- update-with-current-editor-state - [{:keys [id] :as shape}] - (let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state)) - editor-state (mf/deref editor-state-ref) - - content (:content shape) - editor-content - (when editor-state - (-> editor-state - (ted/get-editor-current-content) - (ted/export-content)))] - - (cond-> shape - (some? editor-content) - (assoc :content (attrs/merge content editor-content))))) - -(mf/defc text-resize-content - {::mf/wrap-props false} - [props] - (let [{:keys [id name grow-type] :as shape} (obj/get props "shape") - - ;; NOTE: this breaks the hooks rule of "no hooks inside - ;; conditional code"; but we ensure that this component will - ;; not reused if edition flag is changed with `:key` prop. - ;; Without the `:key` prop combining the shape-id and the - ;; edition flag, this will result in a react error. This is - ;; done for performance reason; with this change only the - ;; shape with edition flag is watching the editor state ref. - shape (cond-> shape - (true? (obj/get props "edition?")) - (update-with-current-editor-state)) - - mnt (mf/use-ref true) - paragraph-ref (mf/use-state nil) - - handle-resize-text - (mf/use-callback - (mf/deps id) - (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"])] - (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) - (log/debug :msg "Resize detected" :shape-id id :width width :height height) - (st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height))))))))) - - text-ref-cb - (mf/use-callback - (mf/deps handle-resize-text) - (fn [node] - (when node - (timers/schedule - #(when (mf/ref-val mnt) - (when-let [ps-node (dom/query node ".paragraph-set")] - (reset! paragraph-ref ps-node)))))))] - - (mf/use-effect - (mf/deps @paragraph-ref handle-resize-text grow-type) - (fn [] - (when-let [paragraph-node @paragraph-ref] - (let [sub (->> (wapi/observe-resize paragraph-node) - (rx/observe-on :af) - (rx/subs handle-resize-text))] - (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) - (fn [] - (rx/dispose! sub)))))) - - (mf/use-effect - (fn [] #(mf/set-ref-val! mnt false))) - - [:& fo/text-shape {:ref text-ref-cb - :shape shape - :grow-type (:grow-type shape) - :key (str "shape-" (:id shape))}])) - - (mf/defc text-wrapper {::mf/wrap-props false} [props] - (let [{:keys [id position-data] :as shape} (unchecked-get props "shape") - edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local)) - edition? (mf/deref edition-ref) + (let [shape (unchecked-get props "shape") - local-position-data (mf/use-state nil) + text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) - sid-ref (mf/use-ref nil) + text-modifier + (mf/deref text-modifier-ref) - handle-change-foreign-object - (mf/use-callback - (fn [node] - (when-let [position-data (utp/calc-position-data node)] - (let [parent (dom/get-parent node) - parent-transform (dom/get-attribute parent "transform") - node-transform (dom/get-attribute node "transform") - - parent-mtx (usvg/parse-transform parent-transform) - node-mtx (usvg/parse-transform node-transform) - - ;; We need to see what transformation is applied in the DOM to reverse it - ;; before calculating the position data - mtx (-> (gmt/multiply parent-mtx node-mtx) - (gmt/inverse)) - - position-data - (->> position-data - (mapv #(merge % (-> (select-keys % [:x :y :width :height]) - (gsh/transform-rect mtx)))))] - (reset! local-position-data position-data))))) - - [node-ref on-change-node] (use-mutable-observer handle-change-foreign-object) - - show-svg-text? (or (some? position-data) (some? @local-position-data)) - - shape - (cond-> shape - (some? @local-position-data) - (assoc :position-data @local-position-data)) - - update-position-data - (fn [] - (when (some? @local-position-data) - (reset! local-position-data nil) - (st/emit! (dch/update-shapes - [id] - (fn [shape] - (-> shape - (assoc :position-data @local-position-data))) - {:save-undo? false}))))] - - (mf/use-layout-effect - (mf/deps @local-position-data) - (fn [] - ;; Timer to update the shape. We do this so a lot of changes won't produce - ;; a lot of updates (kind of a debounce) - (let [sid (timers/schedule 50 update-position-data)] - (fn [] - (rx/dispose! sid))))) - - (mf/use-layout-effect - (mf/deps show-svg-text?) - (fn [] - (when-not show-svg-text? - ;; There is no position data we need to calculate it even if no change has happened - ;; this usualy happens the first time a text is rendered - (let [update-data - (fn update-data [] - (let [node (mf/ref-val node-ref)] - (if (some? node) - (let [position-data (utp/calc-position-data node)] - (reset! local-position-data position-data)) - - ;; No node present, we need to keep waiting - (do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid)) - (when-not @local-position-data - (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))] - (mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))) - - (fn [] - (when-let [sid (mf/ref-val sid-ref)] - (rx/dispose! sid))))) + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier))] [:> shape-container {:shape shape} - ;; We keep hidden the shape when we're editing so it keeps track of the size - ;; and updates the selrect accordingly [:* - [:g.text-shape {:ref on-change-node - :opacity (when show-svg-text? 0) - :pointer-events "none"} + [:g.text-shape + [:& text/text-shape {:shape shape}]] - ;; The `:key` prop here is mandatory because the - ;; text-resize-content breaks a hooks rule and we can't reuse - ;; the component if the edition flag changes. - [:& text-resize-content {:shape - (cond-> shape - show-svg-text? - (dissoc :transform :transform-inverse)) - :edition? edition? - :key (str id edition?)}]] - - (when show-svg-text? - [:g.text-svg {:pointer-events "none"} - [:& svg/text-shape {:shape shape}]]) - - (when (debug? :text-outline) + (when (and (debug? :text-outline) (d/not-empty? (:position-data shape))) (for [data (:position-data shape)] (let [{:keys [x y width height]} data] [:* diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index 4918d52ac..44ff09f65 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.shapes.text.editor (:require ["draft-js" :as draft] + [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.text :as txt] @@ -57,14 +58,13 @@ :shape shape}} nil))) -(defn styles-fn [styles content] - (if (= (.getText content) "") - (-> (.getData content) - (.toJS) - (js->clj :keywordize-keys true) - (sts/generate-text-styles {:show-text? false})) - (-> (txt/styles-to-attrs styles) - (sts/generate-text-styles {:show-text? false})))) +(defn styles-fn [shape styles content] + (let [data (if (= (.getText content) "") + (-> (.getData content) + (.toJS) + (js->clj :keywordize-keys true)) + (txt/styles-to-attrs styles))] + (sts/generate-text-styles shape data {:show-text? false}))) (def default-decorator (ted/create-decorator "PENPOT_SELECTION" selection-component)) @@ -96,6 +96,16 @@ state (get state-map id empty-editor-state) self-ref (mf/use-ref) + text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (mf/deref text-modifier-ref) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier)) + blurred (mf/use-var false) on-key-up @@ -227,7 +237,7 @@ :handle-return handle-return :strip-pasted-styles true :handle-pasted-text handle-pasted-text - :custom-style-fn styles-fn + :custom-style-fn (partial styles-fn shape) :block-renderer-fn #(render-block % shape) :ref on-editor :editor-state state}]])) @@ -252,15 +262,20 @@ position (-> (gpt/point (-> shape :selrect :x) (-> shape :selrect :y)) - (translate-point-from-viewport (mf/ref-val viewport-ref) zoom))] + (translate-point-from-viewport (mf/ref-val viewport-ref) zoom)) + + top-left-corner (gpt/point (/ (:width shape) 2) (/ (:height shape) 2)) + + transform + (-> (gmt/matrix) + (gmt/scale (gpt/point zoom)) + (gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))] [:div {:style {:position "absolute" :left (str (:x position) "px") :top (str (:y position) "px") :pointer-events "all" - :transform (str (gsh/transform-matrix shape nil (gpt/point 0 0))) - :transform-origin "center center"}} + :transform (str transform) + :transform-origin "left top"}} - [:div {:style {:transform (str "scale(" zoom ")") - :transform-origin "top left"}} - [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]])) + [:& text-shape-edit-html {:shape shape :key (str (:id shape))}]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs new file mode 100644 index 000000000..ba2bfa059 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/text/text_edition_outline.cljs @@ -0,0 +1,37 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.text.text-edition-outline + (:require + [app.common.geom.shapes :as gsh] + [app.main.data.workspace.texts :as dwt] + [app.main.refs :as refs] + [rumext.alpha :as mf])) + +(mf/defc text-edition-outline + [{:keys [shape zoom]}] + (let [text-modifier-ref + (mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (mf/deref text-modifier-ref) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier)) + + transform (gsh/transform-matrix shape {:no-flip true}) + {:keys [x y width height]} shape] + + [:rect.main.viewport-selrect + {:x x + :y y + :width width + :height height + :transform (str transform) + :style {:stroke "var(--color-select)" + :stroke-width (/ 1 zoom) + :fill "none"}}])) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs new file mode 100644 index 000000000..17e1aaae6 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts.cljs @@ -0,0 +1,207 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.shapes.text.viewport-texts + (:require + [app.common.attrs :as attrs] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.math :as mth] + [app.common.pages.helpers :as cph] + [app.common.text :as txt] + [app.main.data.workspace.changes :as dch] + [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.hooks :as hooks] + [app.main.ui.shapes.text.fo-text :as fo] + [app.util.dom :as dom] + [app.util.object :as obj] + [app.util.text-editor :as ted] + [app.util.text-svg-position :as utp] + [app.util.timers :as ts] + [rumext.alpha :as mf])) + +(defn- update-with-editor-state + "Updates the shape with the current state in the editor" + [shape editor-state] + (let [content (:content shape) + editor-content + (when editor-state + (-> editor-state + (ted/get-editor-current-content) + (ted/export-content)))] + + (cond-> shape + (and (some? shape) (some? editor-content)) + (assoc :content (attrs/merge content editor-content))))) + +(defn- update-text-shape + [{:keys [grow-type id]} node] + ;; Check if we need to update the size because it's auto-width or auto-height + (when (contains? #{:auto-height :auto-width} grow-type) + (let [{:keys [width height]} + (-> (dom/query node ".paragraph-set") + (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (st/emit! (dwt/resize-text id width height))))) + + ;; Update the position-data of every text fragment + (let [position-data (utp/calc-position-data node)] + (st/emit! (dch/update-shapes + [id] + (fn [shape] + (-> shape + (assoc :position-data position-data))) + {:save-undo? false})))) + +(defn- update-text-modifier + [{:keys [grow-type id]} node] + + (let [position-data (utp/calc-position-data node) + props {:position-data position-data} + + props + (if (contains? #{:auto-height :auto-width} grow-type) + (let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size)) + width (mth/ceil width) + height (mth/ceil height)] + (if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (assoc props :width width :height height) + props)) + props)] + + (st/emit! (dwt/update-text-modifier id props)))) + +(mf/defc text-container + {::mf/wrap-props false} + [props] + (let [shape (obj/get props "shape") + on-update (obj/get props "on-update") + watch-edits (obj/get props "watch-edits") + + handle-update + (mf/use-callback + (mf/deps shape on-update) + (fn [node] + (when (some? node) + (on-update shape node)))) + + text-modifier-ref + (mf/use-memo + (mf/deps (:id shape)) + #(refs/workspace-text-modifier-by-id (:id shape))) + + text-modifier + (when watch-edits (mf/deref text-modifier-ref)) + + shape (cond-> shape + (some? text-modifier) + (dwt/apply-text-modifier text-modifier))] + + [:& fo/text-shape {:key (str "shape-" (:id shape)) + :ref handle-update + :shape shape + :grow-type (:grow-type shape)}])) + +(mf/defc viewport-texts-wrapper + {::mf/wrap-props false + ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} + [props] + (let [text-shapes (obj/get props "text-shapes") + prev-text-shapes (hooks/use-previous text-shapes) + + ;; A change in position-data won't be a "real" change + text-change? + (fn [id] + (let [old-shape (get prev-text-shapes id) + new-shape (get text-shapes id)] + (and (not (identical? old-shape new-shape)) + (not= old-shape new-shape)))) + + changed-texts + (mf/use-memo + (mf/deps text-shapes) + #(->> (keys text-shapes) + (filter text-change?) + (map (d/getf text-shapes)))) + + handle-update-shape (mf/use-callback update-text-shape)] + + [:* + (for [{:keys [id] :as shape} changed-texts] + [:& text-container {:shape shape + :on-update handle-update-shape + :key (str (dm/str "text-container-" id))}])])) + +(defn strip-position-data [[id shape]] + (let [shape (dissoc shape :position-data :transform :transform-inverse)] + [id shape])) + + +(mf/defc viewport-text-editing + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + + ;; Join current objects with the state of the editor + editor-state + (-> (mf/deref refs/workspace-editor-state) + (get (:id shape))) + + shape (cond-> shape + (some? editor-state) + (update-with-editor-state editor-state)) + + handle-update-shape (mf/use-callback update-text-modifier)] + + (mf/use-effect + (mf/deps (:id shape)) + (fn [] + #(st/emit! (dwt/remove-text-modifier (:id shape))))) + + [:& text-container {:shape shape + :watch-edits true + :on-update handle-update-shape}])) + +(defn check-props + [new-props old-props] + (and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects")) + (= (unchecked-get new-props "edition") (unchecked-get old-props "edition")))) + +(mf/defc viewport-texts + {::mf/wrap-props false + ::mf/wrap [#(mf/memo' % check-props)]} + [props] + (let [objects (obj/get props "objects") + edition (obj/get props "edition") + + xf-texts (comp (filter (comp cph/text-shape? second)) + (map strip-position-data)) + + text-shapes + (mf/use-memo + (mf/deps objects) + #(into {} xf-texts objects)) + + editing-shape (get text-shapes edition)] + + ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are + ;; edited + (mf/use-effect + (fn [] + (let [text-nodes (->> text-shapes (vals)(mapcat #(txt/node-seq txt/is-text-node? (:content %)))) + fonts (into #{} (keep :font-id) text-nodes)] + (run! fonts/ensure-loaded! fonts)))) + + [:* + (when editing-shape + [:& viewport-text-editing {:shape editing-shape}]) + [:& viewport-texts-wrapper {:text-shapes text-shapes}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 3633b1674..084dfc441 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.layers (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace :as dw] @@ -244,7 +245,7 @@ (mf/defc frame-wrapper {::mf/wrap-props false - ::mf/wrap [#(mf/memo' % (mf/check-props ["selected" "item" "index" "objects"])) + ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} [props] [:> layer-item props]) @@ -273,48 +274,19 @@ :objects objects :key id}])))]])) -(defn- strip-obj-data [obj] - (select-keys obj [:id - :name - :blocked - :hidden - :shapes - :type - :content - :parent-id - :component-id - :component-file - :shape-ref - :touched - :metadata - :masked-group? - :bool-type])) - -(defn- strip-objects - "Remove unnecesary data from objects map" - [objects] - (persistent! - (->> objects - (reduce-kv - (fn [res id obj] - (assoc! res id (strip-obj-data obj))) - (transient {}))))) - (mf/defc layers-tree-wrapper {::mf/wrap-props false ::mf/wrap [mf/memo #(mf/throttle % 200)]} [props] - (let [search (obj/get props "search") + (let [search (obj/get props "search") filters (obj/get props "filters") filters (if (some #{:shape} filters) (conj filters :rect :circle :path :bool) filters) objects (-> (obj/get props "objects") (hooks/use-equal-memo)) - objects (mf/use-memo - (mf/deps objects) - #(strip-objects objects)) + ;; TODO: Fix performance reparented-objects (d/mapm (fn [_ val] (assoc val :parent-id uuid/zero :shapes nil)) objects) @@ -463,7 +435,7 @@ [:span {:on-click toggle-search} i/search]])) [:div.tool-window-content {:on-scroll on-scroll} - [:& layers-tree-wrapper {:key (:id page) - :objects objects + [:& layers-tree-wrapper {:objects objects + :key (dm/str (:id page)) :search (:search-text @filter-state) :filters (keys (:active-filters @filter-state))}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 8741f3dce..e013ea955 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -144,6 +144,7 @@ :id (:fill-color-ref-id value) :file-id (:fill-color-ref-file value) :gradient (:fill-color-gradient value)} + :key index :index index :title (tr "workspace.options.fill") :on-change (on-change index) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 876b7f2c1..e18ad8832 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.data.macros :as dm] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.hooks :as h] @@ -171,7 +172,8 @@ (seq (:strokes values)) [:& h/sortable-container {} (for [[index value] (d/enumerate (:strokes values []))] - [:& stroke-row {:stroke value + [:& stroke-row {:key (dm/str "stroke-" index) + :stroke value :title (tr "workspace.options.stroke-color") :index index :show-caps show-caps diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 2d4a4f6b5..3967aa506 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -8,6 +8,7 @@ (:require ["react-virtualized" :as rvt] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.pages.helpers :as cph] [app.common.text :as txt] @@ -193,8 +194,8 @@ [:hr] [* [:p.title (tr "workspace.options.recent-fonts")] - (for [font recent-fonts] - [:& font-item {:key (:id font) + (for [[idx font] (d/enumerate recent-fonts)] + [:& font-item {:key (dm/str "font-" idx) :font font :style {} :on-click on-select-and-close diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 3efb15080..5f8e0d0af 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -69,11 +69,11 @@ shared-libs (mf/deref refs/workspace-libraries) hover-detach (mf/use-state false) - get-color-name (fn [{:keys [id file-id]}] - (let [src-colors (if (= file-id current-file-id) - file-colors - (get-in shared-libs [file-id :data :colors]))] - (get-in src-colors [id :name]))) + src-colors (if (= (:file-id color) current-file-id) + file-colors + (get-in shared-libs [(:file-id color) :data :colors])) + + color-name (get-in src-colors [(:id color) :name]) parse-color (fn [color] (-> color @@ -147,15 +147,18 @@ :dnd-over-top (= (:over dprops) :top) :dnd-over-bot (= (:over dprops) :bot)) :ref dref} - [:& cb/color-bullet {:color color + [:& cb/color-bullet {:color (cond-> color + (nil? color-name) (assoc + :id nil + :file-id nil)) :on-click handle-click-color}] (cond ;; Rendering a color with ID - (and (:id color) (not (uc/multiple? color))) + (and (:id color) color-name (not (uc/multiple? color))) [:* [:div.color-info - [:div.color-name (str (get-color-name color))]] + [:div.color-name (str color-name)]] (when on-detach [:div.element-set-actions-button {:on-mouse-enter #(reset! hover-detach true) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 9c4d52ed1..f75698f58 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as clr] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.main.refs :as refs] [app.main.ui.context :as ctx] @@ -17,6 +18,8 @@ [app.main.ui.shapes.export :as use] [app.main.ui.workspace.shapes :as shapes] [app.main.ui.workspace.shapes.text.editor :as editor] + [app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]] + [app.main.ui.workspace.shapes.text.viewport-texts :as stv] [app.main.ui.workspace.viewport.actions :as actions] [app.main.ui.workspace.viewport.comments :as comments] [app.main.ui.workspace.viewport.drawarea :as drawarea] @@ -33,7 +36,6 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] - [app.main.ui.workspace.viewport.thumbnail-renderer :as wtr] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.widgets :as widgets] [beicon.core :as rx] @@ -67,9 +69,13 @@ drawing (mf/deref refs/workspace-drawing) options (mf/deref refs/workspace-page-options) focus (mf/deref refs/workspace-focus-selected) - base-objects (-> (mf/deref refs/workspace-page-objects) + + objects-ref (mf/use-memo #(refs/workspace-page-objects-by-id page-id)) + base-objects (-> (mf/deref objects-ref) (ui-hooks/with-focus-objects focus)) + modifiers (mf/deref refs/workspace-modifiers) + objects-modified (mf/with-memo [base-objects modifiers] (gsh/merge-modifiers base-objects modifiers)) @@ -154,14 +160,14 @@ (>= zoom 8)) show-presence? page-id show-prototypes? (= options-mode :prototype) - show-selection-handlers? (seq selected) + show-selection-handlers? (and (seq selected) (not edition)) show-snap-distance? (and (contains? layout :dynamic-alignment) (= transform :move) (seq selected)) show-snap-points? (and (or (contains? layout :dynamic-alignment) (contains? layout :snap-grid)) (or drawing-obj transform)) - show-selrect? (and selrect (empty? drawing)) + show-selrect? (and selrect (empty? drawing) (not edition)) show-measures? (and (not transform) (not node-editing?) show-distances?) show-artboard-names? (contains? layout :display-artboard-names) show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui))) @@ -177,14 +183,10 @@ (hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom) (hooks/setup-viewport-modifiers modifiers base-objects) (hooks/setup-shortcuts node-editing? drawing-path?) - (hooks/setup-active-frames base-objects vbox hover active-frames) + (hooks/setup-active-frames base-objects vbox hover active-frames zoom) [:div.viewport [:div.viewport-overlays {:ref overlays-ref} - - [:& wtr/frame-renderer {:objects base-objects - :background background}] - (when show-text-editor? [:& editor/text-editor-viewport {:shape editing-shape :viewport-ref viewport-ref @@ -230,6 +232,22 @@ :objects base-objects :active-frames @active-frames}]]]] + [:svg.render-shapes + {:id "text-position-layer" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :preserveAspectRatio "xMidYMid meet" + :key (str "text-position-layer" page-id) + :width (:width vport 0) + :height (:height vport 0) + :view-box (utils/format-viewbox vbox)} + + [:g {:pointer-events "none" :opacity 0} + [:& stv/viewport-texts {:key (dm/str "texts-" page-id) + :page-id page-id + :objects base-objects + :edition edition}]]] + [:svg.viewport-controls {:xmlns "http://www.w3.org/2000/svg" :xmlnsXlink "http://www.w3.org/1999/xlink" @@ -277,6 +295,10 @@ :on-move-selected on-move-selected :on-context-menu on-menu-selected}]) + (when show-text-editor? + [:& text-edition-outline + {:shape (get base-objects edition)}]) + (when show-measures? [:& msr/measurement {:bounds vbox @@ -356,14 +378,6 @@ {:zoom zoom :tooltip tooltip}]) - (when show-prototypes? - [:& interactions/interactions - {:selected selected - :zoom zoom - :objects objects-modified - :current-transform transform - :hover-disabled? hover-disabled?}]) - (when show-selrect? [:& widgets/selection-rect {:data selrect :zoom zoom}]) @@ -410,4 +424,12 @@ :shapes selected-shapes :zoom zoom :edition edition - :disable-handlers (or drawing-tool edition @space?)}]])]]])) + :disable-handlers (or drawing-tool edition @space?)}]]) + + (when show-prototypes? + [:& interactions/interactions + {:selected selected + :zoom zoom + :objects objects-modified + :current-transform transform + :hover-disabled? hover-disabled?}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index dd720dd6a..0a77243ce 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -112,10 +112,7 @@ (when (and left-click? (not mod?) (not shift?) - (not @space?) - (or (not @hover) - (= :frame (:type @hover)) - (some #(contains? selected %) @hover-ids))) + (not @space?)) (dom/prevent-default bevent) (dom/stop-propagation bevent) (st/emit! (dw/start-move-selected))))))) @@ -258,7 +255,7 @@ ;; We store this so in Firefox the middle button won't do a paste of the content (reset! disable-paste true) (timers/schedule #(reset! disable-paste false))) - + (st/emit! (dw/finish-panning) (dw/finish-zooming)))))) @@ -396,7 +393,7 @@ (* unit) (/ zoom))] (if (or ctrl? mod?) - (let [delta (* -1 (+ (.-deltaY ^js event) (.-deltaX ^js event))) + (let [delta (* -1 (+ delta-y delta-x)) scale (-> (+ 1 (/ delta 100)) (mth/clamp 0.77 1.3))] (st/emit! (dw/set-zoom pt scale))) (if (and (not (cfg/check-platform? :macos)) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index d2e70e878..a8890c0b4 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -6,10 +6,10 @@ (ns app.main.ui.workspace.viewport.hooks (:require - [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] [app.main.data.shortcuts :as dsc] [app.main.data.workspace :as dw] [app.main.data.workspace.path.shortcuts :as psc] @@ -17,10 +17,12 @@ [app.main.store :as st] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] + [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as sfd] [app.main.ui.workspace.viewport.actions :as actions] [app.main.ui.workspace.viewport.utils :as utils] [app.main.worker :as uw] [app.util.dom :as dom] + [app.util.globals :as globals] [app.util.timers :as timers] [beicon.core :as rx] [goog.events :as events] @@ -197,47 +199,18 @@ (defn setup-viewport-modifiers [modifiers objects] - (let [transforms + (let [root-frame-ids (mf/use-memo - (mf/deps modifiers) + (mf/deps objects) (fn [] - (when (some? modifiers) - (d/mapm (fn [id {modifiers :modifiers}] - (let [center (gsh/center-shape (get objects id))] - (gsh/modifiers->transform center modifiers))) - modifiers)))) - - shapes - (mf/use-memo - (mf/deps transforms) - (fn [] - (->> (keys transforms) - (mapv (d/getf objects))))) - - prev-shapes (mf/use-var nil) - prev-modifiers (mf/use-var nil) - prev-transforms (mf/use-var nil)] - - ;; Layout effect is important so the code is executed before the modifiers - ;; are applied to the shape - (mf/use-layout-effect - (mf/deps transforms) - (fn [] - (when (and (nil? @prev-transforms) - (some? transforms)) - (utils/start-transform! shapes)) - - (when (some? modifiers) - (utils/update-transform! shapes transforms modifiers)) - - - (when (and (some? @prev-modifiers) - (not (some? modifiers))) - (utils/remove-transform! @prev-shapes)) - - (reset! prev-modifiers modifiers) - (reset! prev-transforms transforms) - (reset! prev-shapes shapes))))) + (let [frame? (into #{} (cph/get-frames-ids objects)) + ;; Removes from zero/shapes attribute all the frames so we can ask only for + ;; the non-frame children + objects (-> objects + (update-in [uuid/zero :shapes] #(filterv (comp not frame?) %)))] + (cph/get-children-ids objects uuid/zero)))) + modifiers (select-keys modifiers root-frame-ids)] + (sfd/use-dynamic-modifiers objects globals/document modifiers))) (defn inside-vbox [vbox objects frame-id] (let [frame (get objects frame-id)] @@ -246,7 +219,7 @@ (gsh/overlaps? frame vbox)))) (defn setup-active-frames - [objects vbox hover active-frames] + [objects vbox hover active-frames zoom] (mf/use-effect (mf/deps vbox) @@ -262,13 +235,16 @@ (reduce-kv set-active-frames {} active-frames)))))) (mf/use-effect - (mf/deps @hover @active-frames) + (mf/deps @hover @active-frames zoom) (fn [] (let [frame-id (if (= :frame (:type @hover)) (:id @hover) (:frame-id @hover))] - (when (not (contains? @active-frames frame-id)) - (swap! active-frames assoc frame-id true)))))) + (if (< zoom 0.25) + (when (some? @active-frames) + (reset! active-frames nil)) + (when (and (some? frame-id)(not (contains? @active-frames frame-id))) + (reset! active-frames {frame-id true}))))))) ;; NOTE: this is executed on each page change, maybe we need to move ;; this shortcuts outside the viewport? diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs index e6440786e..20e0eea6d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/rules.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/rules.cljs @@ -8,6 +8,7 @@ (:require [app.common.colors :as colors] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.main.ui.formats :as fmt] @@ -117,20 +118,17 @@ step (calculate-step-size zoom) clip-id (str "clip-rule-" (d/name axis))] - [:* (let [{:keys [x y width height]} (get-background-area vbox zoom axis)] [:rect {:x x :y y :width width :height height :style {:fill rules-background}}]) - [:g.rules {:clipPath (str "url(#" clip-id ")")} + [:g.rules {:clipPath (str "url(#" clip-id ")")} [:defs [:clipPath {:id clip-id} (let [{:keys [x y width height]} (get-clip-area vbox zoom axis)] [:rect {:x x :y y :width width :height height}])]] - - (let [{:keys [start end]} (get-rule-params vbox axis) minv (max start -100000) minv (* (mth/ceil (/ minv step)) step) @@ -140,9 +138,8 @@ (for [step-val (range minv (inc maxv) step)] (let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]} (get-rule-axis step-val vbox zoom axis)] - [:* - [:text {:key (str "text-" (d/name axis) "-" step-val) - :x text-x + [:* {:key (dm/str "text-" (d/name axis) "-" step-val)} + [:text {:x text-x :y text-y :text-anchor "middle" :dominant-baseline "middle" @@ -177,7 +174,7 @@ :height (/ rule-area-size zoom) :style {:fill rules-background :fill-opacity over-number-opacity}}] - + [:text {:x (- (:x1 selection-rect) (/ 4 zoom)) :y (+ (:y vbox) (/ 12 zoom)) :text-anchor "end" @@ -205,7 +202,7 @@ (let [center-x (+ (:x vbox) (/ rule-area-half-size zoom)) center-y (- (+ (:y selection-rect) (/ (:height selection-rect) 2)) (/ rule-area-half-size zoom))] - + [:g {:transform (str "rotate(-90 " center-x "," center-y ")")} [:rect {:x (- center-x (/ (:height selection-rect) 2) (/ rule-area-half-size zoom)) :y (- center-y (/ rule-area-half-size zoom)) @@ -227,7 +224,7 @@ :height (/ rule-area-size zoom) :style {:fill rules-background :fill-opacity over-number-opacity}}] - + [:text {:x (- center-x (/ (:height selection-rect) 2) (/ 15 zoom)) :y center-y :text-anchor "end" diff --git a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs b/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs deleted file mode 100644 index 634c54c89..000000000 --- a/frontend/src/app/main/ui/workspace/viewport/thumbnail_renderer.cljs +++ /dev/null @@ -1,161 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.ui.workspace.viewport.thumbnail-renderer - (:require - [app.common.math :as mth] - [app.main.data.workspace.persistence :as dwp] - [app.main.store :as st] - [app.util.dom :as dom] - [app.util.object :as obj] - [app.util.timers :as timers] - [beicon.core :as rx] - [rumext.alpha :as mf])) - -(mf/defc frame-thumbnail - "Renders the canvas and image for a frame thumbnail and stores its value into the shape" - [{:keys [shape background on-thumbnail-data on-frame-not-found]}] - - (let [thumbnail-img (mf/use-ref nil) - thumbnail-canvas (mf/use-ref nil) - - {:keys [width height]} shape - fixed-width (mth/clamp width 250 2000) - fixed-height (/ (* height fixed-width) width) - - on-dom-rendered - (mf/use-callback - (mf/deps (:id shape)) - (fn [node] - (when node - (let [img-node (mf/ref-val thumbnail-img)] - (timers/schedule-on-idle - #(let [frame-node (dom/get-element (str "shape-" (:id shape))) - thumb-node (dom/query frame-node ".frame-thumbnail") - loading-node (dom/query frame-node "[data-loading=\"true\"]")] - (if (and (some? frame-node) - ;; Not render if the thumbnail is in display - (nil? thumb-node) - ;; Not render if some image is still loading - (nil? loading-node)) - (let [frame-html (-> (js/XMLSerializer.) - (.serializeToString frame-node)) - - ;; We need to wrap the group node into a SVG with a viewbox that matches the selrect of the frame - svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg") - _ (.setAttribute svg-node "version" "1.1") - _ (.setAttribute svg-node "viewBox" (str (:x shape) " " (:y shape) " " (:width shape) " " (:height shape))) - _ (.setAttribute svg-node "width" (:width shape)) - _ (.setAttribute svg-node "height" (:height shape)) - _ (unchecked-set svg-node "innerHTML" frame-html) - xml (-> (js/XMLSerializer.) - (.serializeToString svg-node) - js/encodeURIComponent - js/unescape - js/btoa) - img-src (str "data:image/svg+xml;base64," xml)] - (obj/set! img-node "src" img-src)) - - (on-frame-not-found (:id shape))))))))) - - on-image-load - (mf/use-callback - (mf/deps on-thumbnail-data background) - (fn [] - (let [canvas-node (mf/ref-val thumbnail-canvas) - img-node (mf/ref-val thumbnail-img) - - canvas-context (.getContext canvas-node "2d") - canvas-width (.-width canvas-node) - canvas-height (.-height canvas-node) - - _ (.clearRect canvas-context 0 0 canvas-width canvas-height) - _ (.rect canvas-context 0 0 canvas-width canvas-height) - _ (set! (.-fillStyle canvas-context) background) - _ (.fill canvas-context) - _ (.drawImage canvas-context img-node 0 0 canvas-width canvas-height) - - data (.toDataURL canvas-node "image/jpg" 1)] - (on-thumbnail-data data))))] - - [:div.frame-renderer {:ref on-dom-rendered - :style {:display "none"}} - [:img.thumbnail-img - {:ref thumbnail-img - :width width - :height height - :on-load on-image-load}] - - [:canvas.thumbnail-canvas - {:ref thumbnail-canvas - :width fixed-width - :height fixed-height}]])) - -(mf/defc frame-renderer - "Component in charge of creating thumbnails and storing them" - {::mf/wrap-props false} - [props] - (let [objects (obj/get props "objects") - background (obj/get props "background") - - ;; Id of the current frame being rendered - shape-id (mf/use-state nil) - - ;; This subject will emit a value every time there is a free "slot" to render - ;; a thumbnail - next (mf/use-memo #(rx/behavior-subject :next)) - - render-frame - (mf/use-callback - (fn [frame-id] - (reset! shape-id frame-id))) - - updates-stream - (mf/use-memo - #(let [update-events (rx/filter dwp/update-frame-thumbnail? st/stream)] - (->> (rx/zip update-events next) - (rx/map first)))) - - on-thumbnail-data - (mf/use-callback - (mf/deps @shape-id) - (fn [data] - (reset! shape-id nil) - (timers/schedule - (fn [] - (st/emit! (dwp/update-shape-thumbnail @shape-id data)) - (rx/push! next :next))))) - - on-frame-not-found - (mf/use-callback - (fn [frame-id] - ;; If we couldn't find the frame maybe is still rendering. We push the event again - ;; after a time - (reset! shape-id nil) - (rx/push! next :next) - (timers/schedule-on-idle - 100 - (st/emitf (dwp/update-frame-thumbnail frame-id)))))] - - (mf/use-effect - (mf/deps render-frame) - (fn [] - (let [sub (->> updates-stream - (rx/subs #(render-frame (-> (deref %) :frame-id))))] - - #(rx/dispose! sub)))) - - (mf/use-layout-effect - (fn [] - (timers/schedule-on-idle - #(st/emit! (dwp/watch-state-changes))))) - - (when (and (some? @shape-id) (contains? objects @shape-id)) - [:& frame-thumbnail {:key (str "thumbnail-" @shape-id) - :shape (get objects @shape-id) - :background background - :on-thumbnail-data on-thumbnail-data - :on-frame-not-found on-frame-not-found}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index f4151f11f..03ee9365d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -77,8 +77,8 @@ (defn get-nodes "Retrieve the DOM nodes to apply the matrix transformation" - [{:keys [id type masked-group?]}] - (let [shape-node (dom/get-element (str "shape-" id)) + [base-node {:keys [id type masked-group?]}] + (let [shape-node (dom/query base-node (str "#shape-" id)) frame? (= :frame type) group? (= :group type) @@ -86,7 +86,7 @@ mask? (and group? masked-group?) ;; When the shape is a frame we maybe need to move its thumbnail - thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))] + thumb-node (when frame? (dom/query (str "#thumbnail-" id)))] (cond frame? @@ -108,10 +108,7 @@ text? [shape-node - (dom/query shape-node "foreignObject") - (dom/query shape-node ".text-shape") - (dom/query shape-node ".text-svg") - (dom/query shape-node ".text-clip")] + (dom/query shape-node ".text-shape")] :else [shape-node]))) @@ -132,9 +129,9 @@ (dom/set-attribute! node "height" height))) (defn start-transform! - [shapes] + [base-node shapes] (doseq [shape shapes] - (when-let [nodes (get-nodes shape)] + (when-let [nodes (get-nodes base-node shape)] (doseq [node nodes] (let [old-transform (dom/get-attribute node "transform")] (when (some? old-transform) @@ -168,37 +165,24 @@ (dom/set-attribute! node att (str new-value)))) (defn update-transform! - [shapes transforms modifiers] + [base-node shapes transforms modifiers] (doseq [{:keys [id type] :as shape} shapes] - (when-let [nodes (get-nodes shape)] + (when-let [nodes (get-nodes base-node shape)] (let [transform (get transforms id) modifiers (get-in modifiers [id :modifiers]) - [text-transform text-width text-height] + [text-transform _text-width _text-height] (when (= :text type) - (text-corrected-transform shape transform modifiers)) - - text-width (str text-width) - text-height (str text-height)] + (text-corrected-transform shape transform modifiers))] (doseq [node nodes] (cond ;; Text shapes need special treatment because their resize only change ;; the text area, not the change size/position - (or (dom/class? node "text-shape") - (dom/class? node "text-svg")) + (dom/class? node "text-shape") (when (some? text-transform) (set-transform-att! node "transform" text-transform)) - (or (= (dom/get-tag-name node) "foreignObject") - (dom/class? node "text-clip")) - (let [cur-width (dom/get-attribute node "width") - cur-height (dom/get-attribute node "height")] - (when (and (some? text-width) (not= cur-width text-width)) - (dom/set-attribute! node "width" text-width)) - (when (and (some? text-height) (not= cur-height text-height)) - (dom/set-attribute! node "height" text-height))) - (or (= (dom/get-tag-name node) "mask") (= (dom/get-tag-name node) "filter")) (transform-region! node modifiers) @@ -214,9 +198,9 @@ (set-transform-att! node "transform" transform))))))) (defn remove-transform! - [shapes] + [base-node shapes] (doseq [shape shapes] - (when-let [nodes (get-nodes shape)] + (when-let [nodes (get-nodes base-node shape)] (doseq [node nodes] (when (some? node) (cond diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index bd89007bf..846ac2397 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -112,11 +112,9 @@ (st/emit! (df/fonts-fetched fonts))))) (rx/map (comp :objects second)) (rx/map (fn [objects] - (let [objects (render/adapt-objects-for-shape objects object-id) - bounds (render/get-object-bounds objects object-id) - object (get objects object-id)] + (let [objects (render/adapt-objects-for-shape objects object-id)] {:objects objects - :object (merge object bounds)})))))) + :object object-id})))))) {:keys [objects object]} (use-resource fetch-state)] @@ -132,10 +130,9 @@ (when objects [:& render/object-svg {:objects objects - :object object + :object-id object-id :render-embed? render-embed? - :render-texts? render-texts? - :zoom 1}]))) + :render-texts? render-texts?}]))) (mf/defc objects-svg [{:keys [page-id file-id object-ids render-embed? render-texts?]}] @@ -155,16 +152,13 @@ (when objects (for [object-id object-ids] - (let [objects (render/adapt-objects-for-shape objects object-id) - bounds (render/get-object-bounds objects object-id) - object (merge (get objects object-id) bounds)] + (let [objects (render/adapt-objects-for-shape objects object-id)] [:& render/object-svg {:objects objects :key (str object-id) - :object object + :object-id object-id :render-embed? render-embed? - :render-texts? render-texts? - :zoom 1}]))))) + :render-texts? render-texts?}]))))) (s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index f0eaf40b3..695303cca 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -43,6 +43,12 @@ [^string title] (set! (.-title globals/document) title)) +(defn set-html-theme-color + [^string color scheme] + (let [meta-node (.querySelector js/document "meta[name='theme-color']")] + (.setAttribute meta-node "content" color) + (.setAttribute meta-node "media" (str/format "(prefers-color-scheme: %s)" scheme)))) + (defn set-page-style! [styles] (let [node (first (get-elements-by-tag globals/document "head")) @@ -345,17 +351,41 @@ {:pre [(blob? b)]} (js/URL.createObjectURL b)) +(defn make-node + ([namespace name] + (.createElementNS globals/document namespace name)) + + ([name] + (.createElement globals/document name))) + +(defn node->xml + [node] + (-> (js/XMLSerializer.) + (.serializeToString node))) + +(defn svg->data-uri + [svg] + (assert (string? svg)) + (let [b64 (-> svg + js/encodeURIComponent + js/unescape + js/btoa)] + (dm/str "data:image/svg+xml;base64," b64))) + (defn set-property! [^js node property value] (when (some? node) - (.setAttribute node property value))) + (.setAttribute node property value)) + node) (defn set-text! [^js node text] (when (some? node) - (set! (.-textContent node) text))) + (set! (.-textContent node) text)) + node) (defn set-css-property! [^js node property value] (when (some? node) - (.setProperty (.-style ^js node) property value))) + (.setProperty (.-style ^js node) property value)) + node) (defn capture-pointer [^js event] (when (some? event) @@ -386,7 +416,8 @@ (defn add-class! [^js node class-name] (when (some? node) (let [class-list (.-classList ^js node)] - (.add ^js class-list class-name)))) + (.add ^js class-list class-name))) + node) (defn remove-class! [^js node class-name] (when (some? node)