mirror of
https://github.com/penpot/penpot.git
synced 2025-07-06 00:57:15 +02:00
⚡ Improved performand for text editing
This commit is contained in:
parent
f945a6e649
commit
b7d33041e8
24 changed files with 448 additions and 328 deletions
|
@ -133,6 +133,7 @@
|
||||||
(dm/export gtr/transform-bounds)
|
(dm/export gtr/transform-bounds)
|
||||||
(dm/export gtr/modifiers->transform)
|
(dm/export gtr/modifiers->transform)
|
||||||
(dm/export gtr/empty-modifiers?)
|
(dm/export gtr/empty-modifiers?)
|
||||||
|
(dm/export gtr/move-position-data)
|
||||||
|
|
||||||
;; Constratins
|
;; Constratins
|
||||||
(dm/export gct/calc-child-modifiers)
|
(dm/export gct/calc-child-modifiers)
|
||||||
|
|
|
@ -139,6 +139,18 @@
|
||||||
(:shapes)
|
(:shapes)
|
||||||
(keep lookup)))))
|
(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
|
(defn get-frames
|
||||||
"Retrieves all frame objects as vector. It is not implemented in
|
"Retrieves all frame objects as vector. It is not implemented in
|
||||||
function of `get-immediate-children` for performance reasons. This
|
function of `get-immediate-children` for performance reasons. This
|
||||||
|
@ -474,3 +486,19 @@
|
||||||
[objects frame-id]
|
[objects frame-id]
|
||||||
(let [ids (concat [frame-id] (get-children-ids objects frame-id))]
|
(let [ids (concat [frame-id] (get-children-ids objects frame-id))]
|
||||||
(select-keys objects ids)))
|
(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!)))
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.attrs :as attrs]
|
[app.common.attrs :as attrs]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
|
@ -303,83 +304,27 @@
|
||||||
(defn not-changed? [old-dim new-dim]
|
(defn not-changed? [old-dim new-dim]
|
||||||
(> (mth/abs (- old-dim new-dim)) 0.1))
|
(> (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
|
(defn resize-text
|
||||||
[id new-width new-height]
|
[id new-width new-height]
|
||||||
(ptk/reify ::resize-text
|
(ptk/reify ::resize-text
|
||||||
IDeref
|
|
||||||
(-deref [_]
|
|
||||||
{:id id :width new-width :height new-height})
|
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ _ _]
|
||||||
(let [;; This stream aggregates the events of "resizing"
|
(letfn [(update-fn [shape]
|
||||||
resize-events
|
(let [{:keys [selrect grow-type]} shape
|
||||||
(rx/merge
|
{shape-width :width shape-height :height} selrect
|
||||||
(->> (rx/of (resize-text id new-width new-height)))
|
modifier-width (gsh/resize-modifiers shape :width new-width)
|
||||||
(->> stream (rx/filter (ptk/type? ::resize-text))))
|
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
|
(and (not-changed? shape-height new-height)
|
||||||
stop-buffer (->> resize-events (rx/debounce 100))
|
(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
|
(rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false}))))))
|
||||||
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))))))
|
|
||||||
|
|
||||||
(defn save-font
|
(defn save-font
|
||||||
[data]
|
[data]
|
||||||
|
@ -391,3 +336,46 @@
|
||||||
(not multiple?)
|
(not multiple?)
|
||||||
(assoc-in [:workspace-global :default-font] data))))))
|
(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]))))
|
||||||
|
|
|
@ -401,3 +401,9 @@
|
||||||
(defn thumbnail-frame-data
|
(defn thumbnail-frame-data
|
||||||
[frame-id]
|
[frame-id]
|
||||||
(l/derived #(get % frame-id) thumbnail-data))
|
(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))
|
||||||
|
|
|
@ -220,8 +220,9 @@
|
||||||
|
|
||||||
(mf/defc selection-guides [{:keys [bounds selrect zoom]}]
|
(mf/defc selection-guides [{:keys [bounds selrect zoom]}]
|
||||||
[:g.selection-guides
|
[:g.selection-guides
|
||||||
(for [[x1 y1 x2 y2] (calculate-guides bounds selrect)]
|
(for [[idx [x1 y1 x2 y2]] (d/enumerate (calculate-guides bounds selrect))]
|
||||||
[:line {:x1 x1
|
[:line {:key (dm/str "guide-" idx)
|
||||||
|
:x1 x1
|
||||||
:y1 y1
|
:y1 y1
|
||||||
:x2 x2
|
:x2 x2
|
||||||
:y2 y2
|
:y2 y2
|
||||||
|
|
|
@ -423,8 +423,9 @@
|
||||||
shape (obj/get props "shape")
|
shape (obj/get props "shape")
|
||||||
elem-name (obj/get child "type")
|
elem-name (obj/get child "type")
|
||||||
render-id (mf/use-ctx muc/render-ctx)
|
render-id (mf/use-ctx muc/render-ctx)
|
||||||
|
stroke-id (dm/fmt "strokes-%" (:id shape))
|
||||||
stroke-props (-> (obj/new)
|
stroke-props (-> (obj/new)
|
||||||
(obj/set! "id" (dm/fmt "strokes-%" (:id shape)))
|
(obj/set! "id" stroke-id)
|
||||||
(cond->
|
(cond->
|
||||||
;; There is a blur
|
;; There is a blur
|
||||||
(and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not))
|
(and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not))
|
||||||
|
@ -440,7 +441,7 @@
|
||||||
(for [[index value] (-> (d/enumerate (:strokes shape)) reverse)]
|
(for [[index value] (-> (d/enumerate (:strokes shape)) reverse)]
|
||||||
(let [props (build-stroke-props index child value render-id)
|
(let [props (build-stroke-props index child value render-id)
|
||||||
shape (assoc value :points (:points shape))]
|
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]]))])]))
|
[:> elem-name props]]))])]))
|
||||||
|
|
||||||
(mf/defc shape-custom-strokes
|
(mf/defc shape-custom-strokes
|
||||||
|
|
|
@ -20,9 +20,13 @@
|
||||||
(mf/defc render-text
|
(mf/defc render-text
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [node (obj/get props "node")
|
(let [node (obj/get props "node")
|
||||||
text (:text node)
|
parent (obj/get props "parent")
|
||||||
style (sts/generate-text-styles node)]
|
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}
|
[:span.text-node {:style style}
|
||||||
(if (= text "") "\u00A0" text)]))
|
(if (= text "") "\u00A0" text)]))
|
||||||
|
|
||||||
|
@ -60,7 +64,7 @@
|
||||||
(mf/defc render-node
|
(mf/defc render-node
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [{:keys [type text children] :as node} (obj/get props "node")]
|
(let [{:keys [type text children]} (obj/get props "node")]
|
||||||
(if (string? text)
|
(if (string? text)
|
||||||
[:> render-text props]
|
[:> render-text props]
|
||||||
(let [component (case type
|
(let [component (case type
|
||||||
|
|
|
@ -15,9 +15,8 @@
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defn generate-root-styles
|
(defn generate-root-styles
|
||||||
[shape node]
|
[{:keys [width height]} node]
|
||||||
(let [valign (:vertical-align node "top")
|
(let [valign (:vertical-align node "top")
|
||||||
{:keys [width height]} shape
|
|
||||||
base #js {:height height
|
base #js {:height height
|
||||||
:width width
|
:width width
|
||||||
:fontFamily "sourcesanspro"
|
:fontFamily "sourcesanspro"
|
||||||
|
@ -57,10 +56,10 @@
|
||||||
(some? text-align) (obj/set! "textAlign" text-align))))
|
(some? text-align) (obj/set! "textAlign" text-align))))
|
||||||
|
|
||||||
(defn generate-text-styles
|
(defn generate-text-styles
|
||||||
([data]
|
([shape data]
|
||||||
(generate-text-styles data nil))
|
(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)
|
(let [letter-spacing (:letter-spacing data 0)
|
||||||
text-decoration (:text-decoration data)
|
text-decoration (:text-decoration data)
|
||||||
text-transform (:text-transform data)
|
text-transform (:text-transform data)
|
||||||
|
@ -81,7 +80,7 @@
|
||||||
|
|
||||||
base #js {:textDecoration text-decoration
|
base #js {:textDecoration text-decoration
|
||||||
:textTransform text-transform
|
:textTransform text-transform
|
||||||
:lineHeight (or line-height "inherit")
|
:lineHeight (or line-height "1.2")
|
||||||
:color (if show-text? text-color "transparent")
|
:color (if show-text? text-color "transparent")
|
||||||
:caretColor (or text-color "black")
|
:caretColor (or text-color "black")
|
||||||
:overflowWrap "initial"}
|
:overflowWrap "initial"}
|
||||||
|
@ -99,33 +98,35 @@
|
||||||
(nil? (:fills data))
|
(nil? (:fills data))
|
||||||
[{:fill-color "#000000" :fill-opacity 1}])
|
[{:fill-color "#000000" :fill-opacity 1}])
|
||||||
|
|
||||||
base (cond-> base
|
|
||||||
(some? fills)
|
|
||||||
(obj/set! "--fills" (transit/encode-str fills)))]
|
|
||||||
|
|
||||||
(when (and (string? letter-spacing)
|
font (when (and (string? font-id) (pos? (alength font-id)))
|
||||||
(pos? (alength letter-spacing)))
|
(get fontsdb font-id))
|
||||||
(obj/set! base "letterSpacing" (str letter-spacing "px")))
|
|
||||||
|
|
||||||
(when (and (string? font-size)
|
[font-family font-style font-weight]
|
||||||
(pos? (alength font-size)))
|
(when (some? font)
|
||||||
(obj/set! base "fontSize" (str font-size "px")))
|
(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)
|
(cond-> base
|
||||||
(pos? (alength font-id)))
|
(some? fills)
|
||||||
(fonts/ensure-loaded! font-id)
|
(obj/set! "--fills" (transit/encode-str fills))
|
||||||
(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)))
|
|
||||||
|
|
||||||
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")))))
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.shapes.text.svg-text
|
(ns app.main.ui.shapes.text.svg-text
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
|
@ -57,7 +58,8 @@
|
||||||
|
|
||||||
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
|
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
|
||||||
dominant-bl (when-not (cfg/check-browser? :safari) "ideographic")
|
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
|
:y y
|
||||||
:alignmentBaseline alignment-bl
|
:alignmentBaseline alignment-bl
|
||||||
:dominantBaseline dominant-bl
|
:dominantBaseline dominant-bl
|
||||||
|
|
|
@ -44,7 +44,14 @@
|
||||||
[props]
|
[props]
|
||||||
(let [objects (obj/get props "objects")
|
(let [objects (obj/get props "objects")
|
||||||
active-frames (obj/get props "active-frames")
|
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
|
;; Render font faces only for shapes that are part of the root
|
||||||
;; frame but don't belongs to any other frame.
|
;; frame but don't belongs to any other frame.
|
||||||
|
@ -57,7 +64,7 @@
|
||||||
(if (cph/frame-shape? item)
|
(if (cph/frame-shape? item)
|
||||||
[:& frame-wrapper {:shape item
|
[:& frame-wrapper {:shape item
|
||||||
:key (:id item)
|
:key (:id item)
|
||||||
:objects objects
|
:objects (get frame-objects (:id item))
|
||||||
:thumbnail? (not (get active-frames (:id item) false))}]
|
:thumbnail? (not (get active-frames (:id item) false))}]
|
||||||
|
|
||||||
[:& shape-wrapper {:shape item
|
[:& shape-wrapper {:shape item
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
(ns app.main.ui.workspace.shapes.frame
|
(ns app.main.ui.workspace.shapes.frame
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.pages.helpers :as cph]
|
|
||||||
[app.main.data.workspace.thumbnails :as dwt]
|
[app.main.data.workspace.thumbnails :as dwt]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
|
@ -40,12 +39,19 @@
|
||||||
[:& ff/fontfaces-style {:fonts fonts}]
|
[:& ff/fontfaces-style {:fonts fonts}]
|
||||||
[:> frame-shape {:shape shape :childs childs} ]]]))))
|
[:> 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
|
(defn frame-wrapper-factory
|
||||||
[shape-wrapper]
|
[shape-wrapper]
|
||||||
|
|
||||||
(let [frame-shape (frame-shape-factory shape-wrapper)]
|
(let [frame-shape (frame-shape-factory shape-wrapper)]
|
||||||
(mf/fnc frame-wrapper
|
(mf/fnc frame-wrapper
|
||||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "thumbnail?" "objects"]))]
|
{::mf/wrap [#(mf/memo' % check-props)]
|
||||||
::mf/wrap-props false}
|
::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
|
|
||||||
|
@ -53,16 +59,10 @@
|
||||||
thumbnail? (unchecked-get props "thumbnail?")
|
thumbnail? (unchecked-get props "thumbnail?")
|
||||||
objects (unchecked-get props "objects")
|
objects (unchecked-get props "objects")
|
||||||
|
|
||||||
objects (mf/use-memo
|
|
||||||
(mf/deps objects)
|
|
||||||
#(cph/get-frame-objects objects (:id shape)))
|
|
||||||
|
|
||||||
objects (hooks/use-equal-memo objects)
|
|
||||||
|
|
||||||
fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects))
|
fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects))
|
||||||
fonts (-> fonts (hooks/use-equal-memo))
|
fonts (-> fonts (hooks/use-equal-memo))
|
||||||
|
|
||||||
force-render (mf/use-state false)
|
force-render (mf/use-state false)
|
||||||
|
|
||||||
;; Thumbnail data
|
;; Thumbnail data
|
||||||
frame-id (:id shape)
|
frame-id (:id shape)
|
||||||
|
@ -76,23 +76,30 @@
|
||||||
;; when `true` we've called the mount for the frame
|
;; when `true` we've called the mount for the frame
|
||||||
rendered? (mf/use-var false)
|
rendered? (mf/use-var false)
|
||||||
|
|
||||||
modifiers (fdm/use-dynamic-modifiers shape objects node-ref)
|
;; Modifiers
|
||||||
|
modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id))
|
||||||
|
modifiers (mf/deref modifiers-ref)
|
||||||
|
|
||||||
disable? (d/not-empty? (get-in modifiers [(:id shape) :modifiers]))
|
disable-thumbnail? (d/not-empty? (get-in modifiers [(:id shape) :modifiers]))
|
||||||
|
|
||||||
[on-load-frame-dom thumb-renderer]
|
[on-load-frame-dom thumb-renderer]
|
||||||
(ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable?)
|
(ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable-thumbnail?)
|
||||||
|
|
||||||
on-frame-load
|
on-frame-load
|
||||||
(fns/use-node-store thumbnail? node-ref rendered?)]
|
(fns/use-node-store thumbnail? node-ref rendered?)]
|
||||||
|
|
||||||
|
(fdm/use-dynamic-modifiers objects @node-ref modifiers)
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(fn []
|
(fn []
|
||||||
;; When a change in the data is received a "force-render" event is emited
|
;; When a change in the data is received a "force-render" event is emited
|
||||||
;; that will force the component to be mounted in memory
|
;; that will force the component to be mounted in memory
|
||||||
(->> (dwt/force-render-stream (:id shape))
|
(let [sub
|
||||||
(rx/take-while #(not @rendered?))
|
(->> (dwt/force-render-stream (:id shape))
|
||||||
(rx/subs #(reset! force-render true)))))
|
(rx/take-while #(not @rendered?))
|
||||||
|
(rx/subs #(reset! force-render true)))]
|
||||||
|
#(when sub
|
||||||
|
(rx/dispose! sub)))))
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render)
|
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render)
|
||||||
|
@ -105,8 +112,7 @@
|
||||||
@node-ref)
|
@node-ref)
|
||||||
(when (not @rendered?) (reset! rendered? true)))))
|
(when (not @rendered?) (reset! rendered? true)))))
|
||||||
|
|
||||||
[:g.frame-container {:key "frame-container"
|
[:g.frame-container {:key "frame-container" :ref on-frame-load}
|
||||||
:ref on-frame-load}
|
|
||||||
thumb-renderer
|
thumb-renderer
|
||||||
|
|
||||||
[:g.frame-thumbnail
|
[:g.frame-thumbnail
|
||||||
|
|
|
@ -8,21 +8,13 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.main.refs :as refs]
|
|
||||||
[app.main.ui.workspace.viewport.utils :as utils]
|
[app.main.ui.workspace.viewport.utils :as utils]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(defn use-dynamic-modifiers
|
(defn use-dynamic-modifiers
|
||||||
[shape objects node-ref]
|
[objects node modifiers]
|
||||||
|
|
||||||
(let [frame-modifiers-ref
|
(let [transforms
|
||||||
(mf/use-memo
|
|
||||||
(mf/deps (:id shape))
|
|
||||||
#(refs/workspace-modifiers-by-frame-id (:id shape)))
|
|
||||||
|
|
||||||
modifiers (mf/deref frame-modifiers-ref)
|
|
||||||
|
|
||||||
transforms
|
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps modifiers)
|
(mf/deps modifiers)
|
||||||
(fn []
|
(fn []
|
||||||
|
@ -48,16 +40,15 @@
|
||||||
(fn []
|
(fn []
|
||||||
(when (and (nil? @prev-transforms)
|
(when (and (nil? @prev-transforms)
|
||||||
(some? transforms))
|
(some? transforms))
|
||||||
(utils/start-transform! @node-ref shapes))
|
(utils/start-transform! node shapes))
|
||||||
|
|
||||||
(when (some? modifiers)
|
(when (some? modifiers)
|
||||||
(utils/update-transform! @node-ref shapes transforms modifiers))
|
(utils/update-transform! node shapes transforms modifiers))
|
||||||
|
|
||||||
(when (and (some? @prev-modifiers)
|
(when (and (some? @prev-modifiers)
|
||||||
(empty? modifiers))
|
(empty? modifiers))
|
||||||
(utils/remove-transform! @node-ref @prev-shapes))
|
(utils/remove-transform! node @prev-shapes))
|
||||||
|
|
||||||
(reset! prev-modifiers modifiers)
|
(reset! prev-modifiers modifiers)
|
||||||
(reset! prev-transforms transforms)
|
(reset! prev-transforms transforms)
|
||||||
(reset! prev-shapes shapes)))
|
(reset! prev-shapes shapes)))))
|
||||||
modifiers))
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
canvas-height (.-height canvas-node)]
|
canvas-height (.-height canvas-node)]
|
||||||
(.clearRect canvas-context 0 0 canvas-width canvas-height)
|
(.clearRect canvas-context 0 0 canvas-width canvas-height)
|
||||||
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
||||||
(.toDataURL canvas-node "image/webp" 0.75)))
|
(.toDataURL canvas-node "image/jpeg" 0.8)))
|
||||||
|
|
||||||
(defn use-render-thumbnail
|
(defn use-render-thumbnail
|
||||||
"Hook that will create the thumbnail thata"
|
"Hook that will create the thumbnail thata"
|
||||||
|
@ -48,10 +48,11 @@
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn []
|
(fn []
|
||||||
(let [canvas-node (mf/ref-val frame-canvas-ref)
|
(let [canvas-node (mf/ref-val frame-canvas-ref)
|
||||||
img-node (mf/ref-val frame-image-ref)
|
img-node (mf/ref-val frame-image-ref)]
|
||||||
thumb-data (draw-thumbnail-canvas canvas-node img-node)]
|
(ts/raf
|
||||||
(st/emit! (dw/update-thumbnail id thumb-data))
|
#(let [thumb-data (draw-thumbnail-canvas canvas-node img-node)]
|
||||||
(reset! image-url nil))))
|
(st/emit! (dw/update-thumbnail id thumb-data))
|
||||||
|
(reset! image-url nil))))))
|
||||||
|
|
||||||
on-change
|
on-change
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
|
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
|
||||||
(dom/set-property! "width" width)
|
(dom/set-property! "width" width)
|
||||||
(dom/set-property! "height" height)
|
(dom/set-property! "height" height)
|
||||||
|
(dom/set-property! "fill" "none")
|
||||||
(obj/set! "innerHTML" frame-html))
|
(obj/set! "innerHTML" frame-html))
|
||||||
|
|
||||||
img-src (-> svg-node dom/node->xml dom/svg->data-uri)]
|
img-src (-> svg-node dom/node->xml dom/svg->data-uri)]
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.refs :as refs]
|
||||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||||
[app.main.ui.shapes.text :as text]
|
[app.main.ui.shapes.text :as text]
|
||||||
[debug :refer [debug?]]
|
[debug :refer [debug?]]
|
||||||
|
@ -17,10 +19,22 @@
|
||||||
(mf/defc text-wrapper
|
(mf/defc text-wrapper
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [shape (unchecked-get props "shape")]
|
(let [shape (unchecked-get props "shape")
|
||||||
|
|
||||||
|
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))]
|
||||||
|
|
||||||
[:> shape-container {:shape shape}
|
[:> shape-container {:shape shape}
|
||||||
[:*
|
[:*
|
||||||
[:& text/text-shape {:shape shape}]
|
[:g.text-shape
|
||||||
|
[:& text/text-shape {:shape shape}]]
|
||||||
|
|
||||||
(when (and (debug? :text-outline) (d/not-empty? (:position-data shape)))
|
(when (and (debug? :text-outline) (d/not-empty? (:position-data shape)))
|
||||||
(for [data (:position-data shape)]
|
(for [data (:position-data shape)]
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.workspace.shapes.text.editor
|
(ns app.main.ui.workspace.shapes.text.editor
|
||||||
(:require
|
(:require
|
||||||
["draft-js" :as draft]
|
["draft-js" :as draft]
|
||||||
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
|
@ -57,14 +58,13 @@
|
||||||
:shape shape}}
|
:shape shape}}
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defn styles-fn [styles content]
|
(defn styles-fn [shape styles content]
|
||||||
(if (= (.getText content) "")
|
(let [data (if (= (.getText content) "")
|
||||||
(-> (.getData content)
|
(-> (.getData content)
|
||||||
(.toJS)
|
(.toJS)
|
||||||
(js->clj :keywordize-keys true)
|
(js->clj :keywordize-keys true))
|
||||||
(sts/generate-text-styles {:show-text? false}))
|
(txt/styles-to-attrs styles))]
|
||||||
(-> (txt/styles-to-attrs styles)
|
(sts/generate-text-styles shape data {:show-text? false})))
|
||||||
(sts/generate-text-styles {:show-text? false}))))
|
|
||||||
|
|
||||||
(def default-decorator
|
(def default-decorator
|
||||||
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
||||||
|
@ -96,6 +96,16 @@
|
||||||
state (get state-map id empty-editor-state)
|
state (get state-map id empty-editor-state)
|
||||||
self-ref (mf/use-ref)
|
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)
|
blurred (mf/use-var false)
|
||||||
|
|
||||||
on-key-up
|
on-key-up
|
||||||
|
@ -227,7 +237,7 @@
|
||||||
:handle-return handle-return
|
:handle-return handle-return
|
||||||
:strip-pasted-styles true
|
:strip-pasted-styles true
|
||||||
:handle-pasted-text handle-pasted-text
|
:handle-pasted-text handle-pasted-text
|
||||||
:custom-style-fn styles-fn
|
:custom-style-fn (partial styles-fn shape)
|
||||||
:block-renderer-fn #(render-block % shape)
|
:block-renderer-fn #(render-block % shape)
|
||||||
:ref on-editor
|
:ref on-editor
|
||||||
:editor-state state}]]))
|
:editor-state state}]]))
|
||||||
|
@ -252,15 +262,20 @@
|
||||||
position
|
position
|
||||||
(-> (gpt/point (-> shape :selrect :x)
|
(-> (gpt/point (-> shape :selrect :x)
|
||||||
(-> shape :selrect :y))
|
(-> 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"
|
[:div {:style {:position "absolute"
|
||||||
:left (str (:x position) "px")
|
:left (str (:x position) "px")
|
||||||
:top (str (:y position) "px")
|
:top (str (:y position) "px")
|
||||||
:pointer-events "all"
|
:pointer-events "all"
|
||||||
:transform (str (gsh/transform-matrix shape nil (gpt/point 0 0)))
|
:transform (str transform)
|
||||||
:transform-origin "center center"}}
|
:transform-origin "left top"}}
|
||||||
|
|
||||||
[:div {:style {:transform (str "scale(" zoom ")")
|
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]))
|
||||||
:transform-origin "top left"}}
|
|
||||||
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]))
|
|
||||||
|
|
|
@ -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"}}]))
|
|
@ -11,8 +11,10 @@
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
|
[app.common.text :as txt]
|
||||||
[app.main.data.workspace.changes :as dch]
|
[app.main.data.workspace.changes :as dch]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
|
[app.main.fonts :as fonts]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
|
@ -35,76 +37,171 @@
|
||||||
(ted/export-content)))]
|
(ted/export-content)))]
|
||||||
|
|
||||||
(cond-> shape
|
(cond-> shape
|
||||||
(some? editor-content)
|
(and (some? shape) (some? editor-content))
|
||||||
(assoc :content (attrs/merge content 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/defc text-container
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false}
|
||||||
::mf/wrap [mf/memo
|
|
||||||
#(mf/deferred % ts/idle-then-raf)]}
|
|
||||||
[props]
|
[props]
|
||||||
(let [shape (obj/get props "shape")
|
(let [shape (obj/get props "shape")
|
||||||
|
on-update (obj/get props "on-update")
|
||||||
|
watch-edits (obj/get props "watch-edits")
|
||||||
|
|
||||||
handle-node-rendered
|
handle-update
|
||||||
(fn [node]
|
(mf/use-callback
|
||||||
(when node
|
(mf/deps shape on-update)
|
||||||
;; Check if we need to update the size because it's auto-width or auto-height
|
(fn [node]
|
||||||
(when (contains? #{:auto-height :auto-width} (:grow-type shape))
|
(when (some? node)
|
||||||
(let [{:keys [width height]}
|
(on-update shape node))))
|
||||||
(-> (dom/query node ".paragraph-set")
|
|
||||||
(dom/get-client-size))]
|
|
||||||
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
|
||||||
(st/emit! (dwt/resize-text (:id shape) (mth/ceil width) (mth/ceil height))))))
|
|
||||||
|
|
||||||
;; Update the position-data of every text fragment
|
text-modifier-ref
|
||||||
(let [position-data (utp/calc-position-data node)]
|
(mf/use-memo
|
||||||
(st/emit! (dch/update-shapes
|
(mf/deps (:id shape))
|
||||||
[(:id shape)]
|
#(refs/workspace-text-modifier-by-id (:id shape)))
|
||||||
(fn [shape]
|
|
||||||
(-> shape
|
text-modifier
|
||||||
(assoc :position-data position-data)))
|
(when watch-edits (mf/deref text-modifier-ref))
|
||||||
{:save-undo? false})))))]
|
|
||||||
|
shape (cond-> shape
|
||||||
|
(some? text-modifier)
|
||||||
|
(dwt/apply-text-modifier text-modifier))]
|
||||||
|
|
||||||
[:& fo/text-shape {:key (str "shape-" (:id shape))
|
[:& fo/text-shape {:key (str "shape-" (:id shape))
|
||||||
:ref handle-node-rendered
|
:ref handle-update
|
||||||
:shape shape
|
:shape shape
|
||||||
:grow-type (:grow-type shape)}]))
|
:grow-type (:grow-type shape)}]))
|
||||||
|
|
||||||
(mf/defc viewport-texts
|
(mf/defc viewport-texts-wrapper
|
||||||
[{:keys [objects edition]}]
|
{::mf/wrap-props false
|
||||||
|
::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||||
(let [editor-state (-> (mf/deref refs/workspace-editor-state)
|
[props]
|
||||||
(get edition))
|
(let [text-shapes (obj/get props "text-shapes")
|
||||||
|
|
||||||
text-shapes-ids
|
|
||||||
(mf/use-memo
|
|
||||||
(mf/deps objects)
|
|
||||||
#(->> objects (vals) (filter cph/text-shape?) (map :id)))
|
|
||||||
|
|
||||||
text-shapes
|
|
||||||
(mf/use-memo
|
|
||||||
(mf/deps text-shapes-ids editor-state edition)
|
|
||||||
#(cond-> (select-keys objects text-shapes-ids)
|
|
||||||
(some? editor-state)
|
|
||||||
(d/update-when edition update-with-editor-state editor-state)))
|
|
||||||
|
|
||||||
prev-text-shapes (hooks/use-previous text-shapes)
|
prev-text-shapes (hooks/use-previous text-shapes)
|
||||||
|
|
||||||
;; A change in position-data won't be a "real" change
|
;; A change in position-data won't be a "real" change
|
||||||
text-change?
|
text-change?
|
||||||
(fn [id]
|
(fn [id]
|
||||||
(not= (-> (get text-shapes id)
|
(let [old-shape (get prev-text-shapes id)
|
||||||
(dissoc :position-data))
|
new-shape (get text-shapes id)]
|
||||||
(-> (get prev-text-shapes id)
|
(and (not (identical? old-shape new-shape))
|
||||||
(dissoc :position-data))))
|
(not= old-shape new-shape))))
|
||||||
|
|
||||||
changed-texts
|
changed-texts
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps text-shapes)
|
(mf/deps text-shapes)
|
||||||
#(->> (keys text-shapes)
|
#(->> (keys text-shapes)
|
||||||
(filter text-change?)
|
(filter text-change?)
|
||||||
(map (d/getf text-shapes))))]
|
(map (d/getf text-shapes))))
|
||||||
|
|
||||||
(for [{:keys [id] :as shape} changed-texts]
|
handle-update-shape (mf/use-callback update-text-shape)]
|
||||||
[:& text-container {:shape (dissoc shape :transform :transform-inverse)
|
|
||||||
:key (str (dm/str "text-container-" id))}])))
|
[:*
|
||||||
|
(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}]]))
|
||||||
|
|
|
@ -245,7 +245,7 @@
|
||||||
|
|
||||||
(mf/defc frame-wrapper
|
(mf/defc frame-wrapper
|
||||||
{::mf/wrap-props false
|
{::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)]}
|
#(mf/deferred % ts/idle-then-raf)]}
|
||||||
[props]
|
[props]
|
||||||
[:> layer-item props])
|
[:> layer-item props])
|
||||||
|
@ -274,33 +274,6 @@
|
||||||
:objects objects
|
:objects objects
|
||||||
:key id}])))]]))
|
:key id}])))]]))
|
||||||
|
|
||||||
(defn- strip-obj-data [obj]
|
|
||||||
(dm/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/defc layers-tree-wrapper
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false
|
||||||
::mf/wrap [mf/memo #(mf/throttle % 200)]}
|
::mf/wrap [mf/memo #(mf/throttle % 200)]}
|
||||||
|
@ -312,10 +285,8 @@
|
||||||
filters)
|
filters)
|
||||||
objects (-> (obj/get props "objects")
|
objects (-> (obj/get props "objects")
|
||||||
(hooks/use-equal-memo))
|
(hooks/use-equal-memo))
|
||||||
objects (mf/use-memo
|
|
||||||
(mf/deps objects)
|
|
||||||
#(strip-objects objects))
|
|
||||||
|
|
||||||
|
;; TODO: Fix performance
|
||||||
reparented-objects (d/mapm (fn [_ val]
|
reparented-objects (d/mapm (fn [_ val]
|
||||||
(assoc val :parent-id uuid/zero :shapes nil))
|
(assoc val :parent-id uuid/zero :shapes nil))
|
||||||
objects)
|
objects)
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.colors :as clr]
|
[app.common.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.main.data.workspace.colors :as dc]
|
[app.main.data.workspace.colors :as dc]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.hooks :as h]
|
[app.main.ui.hooks :as h]
|
||||||
|
@ -171,7 +172,8 @@
|
||||||
(seq (:strokes values))
|
(seq (:strokes values))
|
||||||
[:& h/sortable-container {}
|
[:& h/sortable-container {}
|
||||||
(for [[index value] (d/enumerate (:strokes values []))]
|
(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")
|
:title (tr "workspace.options.stroke-color")
|
||||||
:index index
|
:index index
|
||||||
:show-caps show-caps
|
:show-caps show-caps
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
["react-virtualized" :as rvt]
|
["react-virtualized" :as rvt]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
|
@ -193,8 +194,8 @@
|
||||||
[:hr]
|
[:hr]
|
||||||
[*
|
[*
|
||||||
[:p.title (tr "workspace.options.recent-fonts")]
|
[:p.title (tr "workspace.options.recent-fonts")]
|
||||||
(for [font recent-fonts]
|
(for [[idx font] (d/enumerate recent-fonts)]
|
||||||
[:& font-item {:key (:id font)
|
[:& font-item {:key (dm/str "font-" idx)
|
||||||
:font font
|
:font font
|
||||||
:style {}
|
:style {}
|
||||||
:on-click on-select-and-close
|
:on-click on-select-and-close
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
[app.main.ui.shapes.export :as use]
|
[app.main.ui.shapes.export :as use]
|
||||||
[app.main.ui.workspace.shapes :as shapes]
|
[app.main.ui.workspace.shapes :as shapes]
|
||||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
[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.shapes.text.viewport-texts :as stv]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
[app.main.ui.workspace.viewport.comments :as comments]
|
[app.main.ui.workspace.viewport.comments :as comments]
|
||||||
|
@ -159,14 +160,14 @@
|
||||||
(>= zoom 8))
|
(>= zoom 8))
|
||||||
show-presence? page-id
|
show-presence? page-id
|
||||||
show-prototypes? (= options-mode :prototype)
|
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)
|
show-snap-distance? (and (contains? layout :dynamic-alignment)
|
||||||
(= transform :move)
|
(= transform :move)
|
||||||
(seq selected))
|
(seq selected))
|
||||||
show-snap-points? (and (or (contains? layout :dynamic-alignment)
|
show-snap-points? (and (or (contains? layout :dynamic-alignment)
|
||||||
(contains? layout :snap-grid))
|
(contains? layout :snap-grid))
|
||||||
(or drawing-obj transform))
|
(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-measures? (and (not transform) (not node-editing?) show-distances?)
|
||||||
show-artboard-names? (contains? layout :display-artboard-names)
|
show-artboard-names? (contains? layout :display-artboard-names)
|
||||||
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
|
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
|
||||||
|
@ -294,6 +295,10 @@
|
||||||
:on-move-selected on-move-selected
|
:on-move-selected on-move-selected
|
||||||
:on-context-menu on-menu-selected}])
|
:on-context-menu on-menu-selected}])
|
||||||
|
|
||||||
|
(when show-text-editor?
|
||||||
|
[:& text-edition-outline
|
||||||
|
{:shape (get base-objects edition)}])
|
||||||
|
|
||||||
(when show-measures?
|
(when show-measures?
|
||||||
[:& msr/measurement
|
[:& msr/measurement
|
||||||
{:bounds vbox
|
{:bounds vbox
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
(ns app.main.ui.workspace.viewport.hooks
|
(ns app.main.ui.workspace.viewport.hooks
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.common.pages.helpers :as cph]
|
[app.common.pages.helpers :as cph]
|
||||||
|
@ -18,6 +17,7 @@
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.streams :as ms]
|
[app.main.streams :as ms]
|
||||||
[app.main.ui.hooks :as hooks]
|
[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.actions :as actions]
|
||||||
[app.main.ui.workspace.viewport.utils :as utils]
|
[app.main.ui.workspace.viewport.utils :as utils]
|
||||||
[app.main.worker :as uw]
|
[app.main.worker :as uw]
|
||||||
|
@ -199,62 +199,18 @@
|
||||||
|
|
||||||
(defn setup-viewport-modifiers
|
(defn setup-viewport-modifiers
|
||||||
[modifiers objects]
|
[modifiers objects]
|
||||||
|
|
||||||
(let [root-frame-ids
|
(let [root-frame-ids
|
||||||
(mf/use-memo
|
(mf/use-memo
|
||||||
(mf/deps objects)
|
(mf/deps objects)
|
||||||
#(->> objects
|
|
||||||
(vals)
|
|
||||||
(filter (fn [{:keys [type frame-id]}]
|
|
||||||
(and
|
|
||||||
(not= :frame type)
|
|
||||||
(= uuid/zero frame-id))))
|
|
||||||
(map :id)))
|
|
||||||
|
|
||||||
objects (select-keys objects root-frame-ids)
|
|
||||||
modifiers (select-keys modifiers root-frame-ids)
|
|
||||||
|
|
||||||
transforms
|
|
||||||
(mf/use-memo
|
|
||||||
(mf/deps modifiers)
|
|
||||||
(fn []
|
(fn []
|
||||||
(when (some? modifiers)
|
(let [frame? (into #{} (cph/get-frames-ids objects))
|
||||||
(d/mapm (fn [id {modifiers :modifiers}]
|
;; Removes from zero/shapes attribute all the frames so we can ask only for
|
||||||
(let [center (gsh/center-shape (get objects id))]
|
;; the non-frame children
|
||||||
(gsh/modifiers->transform center modifiers)))
|
objects (-> objects
|
||||||
modifiers))))
|
(update-in [uuid/zero :shapes] #(filterv (comp not frame?) %)))]
|
||||||
|
(cph/get-children-ids objects uuid/zero))))
|
||||||
shapes
|
modifiers (select-keys modifiers root-frame-ids)]
|
||||||
(mf/use-memo
|
(sfd/use-dynamic-modifiers objects globals/document modifiers)))
|
||||||
(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! globals/document shapes))
|
|
||||||
|
|
||||||
(when (some? modifiers)
|
|
||||||
(utils/update-transform! globals/document shapes transforms modifiers))
|
|
||||||
|
|
||||||
|
|
||||||
(when (and (some? @prev-modifiers)
|
|
||||||
(not (some? modifiers)))
|
|
||||||
(utils/remove-transform! globals/document @prev-shapes))
|
|
||||||
|
|
||||||
(reset! prev-modifiers modifiers)
|
|
||||||
(reset! prev-transforms transforms)
|
|
||||||
(reset! prev-shapes shapes)))))
|
|
||||||
|
|
||||||
(defn inside-vbox [vbox objects frame-id]
|
(defn inside-vbox [vbox objects frame-id]
|
||||||
(let [frame (get objects frame-id)]
|
(let [frame (get objects frame-id)]
|
||||||
|
|
|
@ -108,10 +108,7 @@
|
||||||
|
|
||||||
text?
|
text?
|
||||||
[shape-node
|
[shape-node
|
||||||
(dom/query shape-node "foreignObject")
|
(dom/query shape-node ".text-shape")]
|
||||||
(dom/query shape-node ".text-shape")
|
|
||||||
(dom/query shape-node ".text-svg")
|
|
||||||
(dom/query shape-node ".text-clip")]
|
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[shape-node])))
|
[shape-node])))
|
||||||
|
@ -174,31 +171,18 @@
|
||||||
(let [transform (get transforms id)
|
(let [transform (get transforms id)
|
||||||
modifiers (get-in modifiers [id :modifiers])
|
modifiers (get-in modifiers [id :modifiers])
|
||||||
|
|
||||||
[text-transform text-width text-height]
|
[text-transform _text-width _text-height]
|
||||||
(when (= :text type)
|
(when (= :text type)
|
||||||
(text-corrected-transform shape transform modifiers))
|
(text-corrected-transform shape transform modifiers))]
|
||||||
|
|
||||||
text-width (str text-width)
|
|
||||||
text-height (str text-height)]
|
|
||||||
|
|
||||||
(doseq [node nodes]
|
(doseq [node nodes]
|
||||||
(cond
|
(cond
|
||||||
;; Text shapes need special treatment because their resize only change
|
;; Text shapes need special treatment because their resize only change
|
||||||
;; the text area, not the change size/position
|
;; the text area, not the change size/position
|
||||||
(or (dom/class? node "text-shape")
|
(dom/class? node "text-shape")
|
||||||
(dom/class? node "text-svg"))
|
|
||||||
(when (some? text-transform)
|
(when (some? text-transform)
|
||||||
(set-transform-att! node "transform" 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")
|
(or (= (dom/get-tag-name node) "mask")
|
||||||
(= (dom/get-tag-name node) "filter"))
|
(= (dom/get-tag-name node) "filter"))
|
||||||
(transform-region! node modifiers)
|
(transform-region! node modifiers)
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
#{:app.main.data.workspace.notifications/handle-pointer-update
|
#{:app.main.data.workspace.notifications/handle-pointer-update
|
||||||
:app.main.data.workspace.selection/change-hover-state})
|
:app.main.data.workspace.selection/change-hover-state})
|
||||||
|
|
||||||
(defonce ^:dynamic *debug* (atom #{#_:events}))
|
(defonce ^:dynamic *debug* (atom #{#_:events :thumbnails}))
|
||||||
|
|
||||||
(defn debug-all! [] (reset! *debug* debug-options))
|
(defn debug-all! [] (reset! *debug* debug-options))
|
||||||
(defn debug-none! [] (reset! *debug* #{}))
|
(defn debug-none! [] (reset! *debug* #{}))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue