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)