diff --git a/.gitignore b/.gitignore index 22ae73c02..b7f152cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ /backend/resources/public/assets /backend/resources/public/media /backend/target/ +/backend/builtin-templates /bundle* /cd.md /clj-profiler/ diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 6885f5cf5..764f0651e 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -728,11 +728,16 @@ [data] (letfn [(process-map-form [form] (cond-> form - ;; Relink Image Shapes + ;; Relink image shapes (and (map? (:metadata form)) (= :image (:type form))) (update-in [:metadata :id] lookup-index) + ;; Relink paths with fill image + (and (map? (:fill-image form)) + (= :path (:type form))) + (update-in [:fill-image :id] lookup-index) + ;; This covers old shapes and the new :fills. (uuid? (:fill-color-ref-file form)) (update :fill-color-ref-file lookup-index) diff --git a/common/src/app/common/pages/focus.cljc b/common/src/app/common/pages/focus.cljc index a7ca0f495..726c102e6 100644 --- a/common/src/app/common/pages/focus.cljc +++ b/common/src/app/common/pages/focus.cljc @@ -40,6 +40,5 @@ (defn is-in-focus? [objects focus id] - (d/seek - #(contains? focus %) - (cph/get-parents-seq objects id))) + (d/seek (partial contains? focus) + (cons id (cph/get-parent-ids objects id)))) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index fb3563c9f..3f5ef1ce4 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -96,16 +96,6 @@ [objects id] (-> objects (get id) :parent-id)) -(defn get-parents-seq - [objects shape-id] - - (cond - (nil? shape-id) - nil - - :else - (lazy-seq (cons shape-id (get-parents-seq objects (get-in objects [shape-id :parent-id])))))) - (defn get-parent-ids "Returns a vector of parents of the specified shape." [objects shape-id] @@ -220,8 +210,8 @@ (defn- get-base [objects id-a id-b] - (let [parents-a (reverse (get-parents-seq objects id-a)) - parents-b (reverse (get-parents-seq objects id-b)) + (let [parents-a (reverse (cons id-a (get-parent-ids objects id-a))) + parents-b (reverse (cons id-b (get-parent-ids objects id-b))) [base base-child-a base-child-b] (loop [parents-a (rest parents-a) @@ -648,7 +638,7 @@ (defn is-child? [objects parent-id candidate-child-id] - (let [parents (get-parents-seq objects candidate-child-id)] + (let [parents (get-parent-ids objects candidate-child-id)] (some? (d/seek #(= % parent-id) parents)))) (defn reduce-objects @@ -688,11 +678,10 @@ (defn get-shape-id-root-frame [objects shape-id] - (->> (get-parents-seq objects shape-id) + (->> (get-parent-ids objects shape-id) + (cons shape-id) (map (d/getf objects)) - (d/seek #(and (= :frame (:type %)) - (= uuid/zero (:frame-id %)))) - + (d/seek root-frame?) :id)) (defn get-viewer-frames diff --git a/frontend/resources/styles/main/partials/viewer.scss b/frontend/resources/styles/main/partials/viewer.scss index 0a774646f..00a0c14bc 100644 --- a/frontend/resources/styles/main/partials/viewer.scss +++ b/frontend/resources/styles/main/partials/viewer.scss @@ -164,6 +164,7 @@ top: 50px; width: 256px; height: 100%; + z-index: 10; } } diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index e09d8eff8..50f6dcff0 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -342,6 +342,13 @@ (some? list) (assoc :list list))))))) +(defn update-options + [params] + (ptk/reify ::update-options + ptk/UpdateEvent + (update [_ state] + (update state :comments-local merge params)))) + (s/def ::create-draft-params (s/keys :req-un [::page-id ::file-id ::position])) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index d920b7685..c8061dac6 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -7,6 +7,7 @@ (ns app.main.data.viewer (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.pages.helpers :as cph] [app.common.spec :as us] @@ -20,6 +21,9 @@ [cljs.spec.alpha :as s] [potok.core :as ptk])) +(s/def ::nilable-boolean (s/nilable ::us/boolean)) +(s/def ::nilable-animation (s/nilable ::ctsi/animation)) + ;; --- Local State Initialization (def ^:private @@ -32,7 +36,6 @@ :comments-show :unresolved :selected #{} :collapsed #{} - :overlays [] :hover nil :share-id "" :file-comments-users []}) @@ -329,7 +332,8 @@ (declare flash-done) -(def flash-interactions +(defn flash-interactions + [] (ptk/reify ::flash-interactions ptk/UpdateEvent (update [_ state] @@ -367,7 +371,7 @@ (ptk/reify ::complete-animation ptk/UpdateEvent (update [_ state] - (d/dissoc-in state [:viewer-local :current-animation])))) + (dissoc state :viewer-animation)))) ;; --- Navigation inside page @@ -376,7 +380,7 @@ (ptk/reify ::go-to-frame-by-index ptk/UpdateEvent (update [_ state] - (assoc-in state [:viewer-local :overlays] [])) + (assoc state :viewer-overlays [])) ptk/WatchEvent (watch [_ state _] @@ -391,8 +395,9 @@ (go-to-frame frame-id nil)) ([frame-id animation] - (us/verify ::us/uuid frame-id) - (us/verify (s/nilable ::ctsi/animation) animation) + (us/assert! ::us/uuid frame-id) + (us/assert! ::nilable-animation animation) + (ptk/reify ::go-to-frame ptk/UpdateEvent (update [_ state] @@ -404,13 +409,13 @@ frame (get frames index)] (cond-> state :always - (assoc-in [:viewer-local :overlays] []) + (assoc :viewer-overlays []) (some? animation) - (assoc-in [:viewer-local :current-animation] - {:kind :go-to-frame - :orig-frame-id (:id frame) - :animation animation})))) + (assoc :viewer-animation + {:kind :go-to-frame + :orig-frame-id (:id frame) + :animation animation})))) ptk/WatchEvent (watch [_ state _] @@ -440,7 +445,7 @@ (ptk/reify ::go-to-section ptk/UpdateEvent (update [_ state] - (assoc-in state [:viewer-local :overlays] [])) + (assoc state :viewer-overlays [])) ptk/WatchEvent (watch [_ state _] @@ -451,64 +456,69 @@ ;; --- Overlays -(defn- do-open-overlay +(defn- open-overlay* [state frame position close-click-outside background-overlay animation] (cond-> state :always - (update-in [:viewer-local :overlays] conj - {:frame frame - :position position - :close-click-outside close-click-outside - :background-overlay background-overlay}) - (some? animation) - (assoc-in [:viewer-local :current-animation] - {:kind :open-overlay - :overlay-id (:id frame) - :animation animation}))) + (update :viewer-overlays conj + {:frame frame + :id (:id frame) + :position position + :close-click-outside close-click-outside + :background-overlay background-overlay}) -(defn- do-close-overlay + (some? animation) + (assoc :viewer-animation + {:kind :open-overlay + :overlay-id (:id frame) + :animation animation}))) + +(defn- close-overlay* [state frame-id animation] (if (nil? animation) - (update-in state [:viewer-local :overlays] - (fn [overlays] - (d/removev #(= (:id (:frame %)) frame-id) overlays))) - (assoc-in state [:viewer-local :current-animation] - {:kind :close-overlay - :overlay-id frame-id - :animation animation}))) + (update state :viewer-overlays + (fn [overlays] + (d/removev #(= (:id (:frame %)) frame-id) overlays))) + (assoc state :viewer-animation + {:kind :close-overlay + :overlay-id frame-id + :animation animation}))) (defn open-overlay [frame-id position close-click-outside background-overlay animation] - (us/verify ::us/uuid frame-id) - (us/verify ::gpt/point position) - (us/verify (s/nilable ::us/boolean) close-click-outside) - (us/verify (s/nilable ::us/boolean) background-overlay) - (us/verify (s/nilable ::ctsi/animation) animation) + (us/assert! ::us/uuid frame-id) + (us/assert! ::gpt/point position) + (us/assert! ::nilable-boolean close-click-outside) + (us/assert! ::nilable-boolean background-overlay) + (us/assert! ::nilable-animation animation) + (ptk/reify ::open-overlay ptk/UpdateEvent (update [_ state] (let [route (:route state) qparams (:query-params route) page-id (:page-id qparams) - frames (get-in state [:viewer :pages page-id :all-frames]) + frames (dm/get-in state [:viewer :pages page-id :all-frames]) frame (d/seek #(= (:id %) frame-id) frames) - overlays (get-in state [:viewer-local :overlays])] + overlays (:viewer-overlays state)] (if-not (some #(= (:frame %) frame) overlays) - (do-open-overlay state - frame - position - close-click-outside - background-overlay - animation) + (open-overlay* state + frame + position + close-click-outside + background-overlay + animation) state))))) + (defn toggle-overlay [frame-id position close-click-outside background-overlay animation] - (us/verify ::us/uuid frame-id) - (us/verify ::gpt/point position) - (us/verify (s/nilable ::us/boolean) close-click-outside) - (us/verify (s/nilable ::us/boolean) background-overlay) - (us/verify (s/nilable ::ctsi/animation) animation) + (us/assert! ::us/uuid frame-id) + (us/assert! ::gpt/point position) + (us/assert! ::nilable-boolean close-click-outside) + (us/assert! ::nilable-boolean background-overlay) + (us/assert! ::nilable-animation animation) + (ptk/reify ::toggle-overlay ptk/UpdateEvent (update [_ state] @@ -517,29 +527,30 @@ page-id (:page-id qparams) frames (get-in state [:viewer :pages page-id :all-frames]) frame (d/seek #(= (:id %) frame-id) frames) - overlays (get-in state [:viewer-local :overlays])] + overlays (:viewer-overlays state)] (if-not (some #(= (:frame %) frame) overlays) - (do-open-overlay state - frame - position - close-click-outside - background-overlay - animation) - (do-close-overlay state - (:id frame) - (ctsi/invert-direction animation))))))) + (open-overlay* state + frame + position + close-click-outside + background-overlay + animation) + (close-overlay* state + (:id frame) + (ctsi/invert-direction animation))))))) (defn close-overlay ([frame-id] (close-overlay frame-id nil)) ([frame-id animation] - (us/verify ::us/uuid frame-id) - (us/verify (s/nilable ::ctsi/animation) animation) + (us/assert! ::us/uuid frame-id) + (us/assert! ::nilable-animation animation) + (ptk/reify ::close-overlay ptk/UpdateEvent (update [_ state] - (do-close-overlay state - frame-id - animation))))) + (close-overlay* state + frame-id + animation))))) ;; --- Objects selection diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index fd65d7569..768a86f1f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -29,6 +29,7 @@ (mf/defc on-main-error [{:keys [error] :as props}] (mf/with-effect + (js/console.log error) (st/emit! (rt/assign-exception error))) [:span "Internal application error"]) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 14361b96f..31fe47767 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.comments (:require + [app.common.geom.point :as gpt] [app.config :as cfg] [app.main.data.comments :as dcm] [app.main.data.modal :as modal] @@ -110,8 +111,10 @@ [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]])])) (mf/defc draft-thread - [{:keys [draft zoom on-cancel on-submit] :as props}] - (let [position (:position draft) + [{:keys [draft zoom on-cancel on-submit position-modifier]}] + (let [position (cond-> (:position draft) + (some? position-modifier) + (gpt/transform position-modifier)) content (:content draft) pos-x (* (:x position) zoom) pos-y (* (:y position) zoom) @@ -281,9 +284,12 @@ (l/derived (l/in [:comments id]) st/state)) (mf/defc thread-comments - [{:keys [thread zoom users origin]}] + {::mf/wrap [mf/memo]} + [{:keys [thread zoom users origin position-modifier]}] (let [ref (mf/use-ref) - pos (:position thread) + pos (cond-> (:position thread) + (some? position-modifier) + (gpt/transform position-modifier)) pos-x (+ (* (:x pos) zoom) 14) pos-y (- (* (:y pos) zoom) 14) @@ -384,9 +390,12 @@ (mf/defc thread-bubble {::mf/wrap [mf/memo]} - [{:keys [thread zoom open? on-click origin]}] - (let [pos (:position thread) - drag? (mf/use-ref nil) + [{:keys [thread zoom open? on-click origin position-modifier]}] + (let [pos (cond-> (:position thread) + (some? position-modifier) + (gpt/transform position-modifier)) + + drag? (mf/use-ref nil) was-open? (mf/use-ref nil) {:keys [on-pointer-down @@ -398,41 +407,45 @@ pos-x (* (or (:new-position-x @state) (:x pos)) zoom) pos-y (* (or (:new-position-y @state) (:y pos)) zoom) - on-pointer-down* (mf/use-callback - (mf/deps origin was-open? open? drag? on-pointer-down) - (fn [event] - (when (not= origin :viewer) - (mf/set-ref-val! was-open? open?) - (when open? (st/emit! (dcm/close-thread))) - (mf/set-ref-val! drag? false) - (dom/stop-propagation event) - (on-pointer-down event)))) + on-pointer-down* + (mf/use-callback + (mf/deps origin was-open? open? drag? on-pointer-down) + (fn [event] + (when (not= origin :viewer) + (mf/set-ref-val! was-open? open?) + (when open? (st/emit! (dcm/close-thread))) + (mf/set-ref-val! drag? false) + (dom/stop-propagation event) + (on-pointer-down event)))) - on-pointer-up* (mf/use-callback - (mf/deps origin thread was-open? drag? on-pointer-up) - (fn [event] - (when (not= origin :viewer) - (dom/stop-propagation event) - (on-pointer-up event thread) + on-pointer-up* + (mf/use-callback + (mf/deps origin thread was-open? drag? on-pointer-up) + (fn [event] + (when (not= origin :viewer) + (dom/stop-propagation event) + (on-pointer-up event thread) - (when (or (and (mf/ref-val was-open?) (mf/ref-val drag?)) - (and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?)))) - (st/emit! (dcm/open-thread thread)))))) + (when (or (and (mf/ref-val was-open?) (mf/ref-val drag?)) + (and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?)))) + (st/emit! (dcm/open-thread thread)))))) - on-mouse-move* (mf/use-callback - (mf/deps origin drag? on-mouse-move) - (fn [event] - (when (not= origin :viewer) - (mf/set-ref-val! drag? true) - (dom/stop-propagation event) - (on-mouse-move event)))) + on-mouse-move* + (mf/use-callback + (mf/deps origin drag? on-mouse-move) + (fn [event] + (when (not= origin :viewer) + (mf/set-ref-val! drag? true) + (dom/stop-propagation event) + (on-mouse-move event)))) - on-click* (mf/use-callback - (mf/deps origin thread on-click) - (fn [event] - (dom/stop-propagation event) - (when (= origin :viewer) - (on-click thread))))] + on-click* + (mf/use-callback + (mf/deps origin thread on-click) + (fn [event] + (dom/stop-propagation event) + (when (= origin :viewer) + (on-click thread))))] [:div.thread-bubble {:style {:top (str pos-y "px") @@ -448,7 +461,7 @@ [:span (:seqn thread)]])) (mf/defc comment-thread - [{:keys [item users on-click] :as props}] + [{:keys [item users on-click]}] (let [owner (get users (:owner-id item)) on-click* (mf/use-callback diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 34a3518c9..92a01bb6b 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -8,12 +8,7 @@ (:require [rumext.alpha :as mf])) -(def render-ctx (mf/create-context nil)) -(def def-ctx (mf/create-context false)) - -;; This content is used to replace complex colors to simple ones -;; for text shapes in the export process -(def text-plain-colors-ctx (mf/create-context false)) +(def render-id (mf/create-context nil)) (def current-route (mf/create-context nil)) (def current-profile (mf/create-context nil)) @@ -21,6 +16,8 @@ (def current-project-id (mf/create-context nil)) (def current-page-id (mf/create-context nil)) (def current-file-id (mf/create-context nil)) -(def scroll-ctx (mf/create-context nil)) -(def active-frames-ctx (mf/create-context nil)) -(def render-thumbnails (mf/create-context nil)) +(def current-scroll (mf/create-context nil)) +(def current-zoom (mf/create-context nil)) + +(def active-frames (mf/create-context nil)) +(def render-thumbnails (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 4b95e4350..f94871a5f 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -159,7 +159,7 @@ (defn add-style-attrs ([props shape] - (let [render-id (mf/use-ctx muc/render-ctx)] + (let [render-id (mf/use-ctx muc/render-id)] (add-style-attrs props shape render-id))) ([props shape render-id] diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index c0eb8ddf4..7753c3f32 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -211,7 +211,7 @@ {::mf/wrap-props false} [props] - (let [render-id (mf/use-ctx muc/render-ctx) + (let [render-id (mf/use-ctx muc/render-id) child (obj/get props "children") base-props (obj/get child "props") elem-name (obj/get child "type") @@ -253,7 +253,7 @@ (mf/defc inner-stroke {::mf/wrap-props false} [props] - (let [render-id (mf/use-ctx muc/render-ctx) + (let [render-id (mf/use-ctx muc/render-id) child (obj/get props "children") base-props (obj/get child "props") elem-name (obj/get child "type") @@ -292,7 +292,7 @@ (let [child (obj/get props "children") shape (obj/get props "shape") - render-id (mf/use-ctx muc/render-ctx) + render-id (mf/use-ctx muc/render-id) index (obj/get props "index") stroke-width (:stroke-width shape 0) stroke-style (:stroke-style shape :none) @@ -417,7 +417,7 @@ shape (obj/get props "shape") elem-name (obj/get child "type") position (or (obj/get props "position") 0) - render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-ctx))] + render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-id))] [:g {:id (dm/fmt "fills-%" (:id shape))} [:> elem-name (build-fill-props shape child position render-id)]])) @@ -427,7 +427,7 @@ (let [child (obj/get props "children") shape (obj/get props "shape") elem-name (obj/get child "type") - render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-ctx)) + render-id (or (obj/get props "render-id") (mf/use-ctx muc/render-id)) stroke-id (dm/fmt "strokes-%" (:id shape)) stroke-props (-> (obj/create) (obj/set! "id" stroke-id) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index 5f8ceba50..b2453f7e0 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -286,7 +286,7 @@ (for [[index fill] (d/enumerate fills)] [:> "penpot:fill" #js {:penpot:fill-color (if (some? (:fill-color-gradient fill)) - (str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index)) + (str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) (d/name (:fill-color fill))) :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) @@ -299,7 +299,7 @@ (for [[index stroke] (d/enumerate strokes)] [:> "penpot:stroke" #js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke)) - (str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index)) + (str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-id) "_" index)) (d/name (:stroke-color stroke))) :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 1aa7a7b5b..ba821a5d7 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -62,7 +62,7 @@ :height height :className "frame-background"})) path? (some? (.-d props)) - render-id (mf/use-ctx muc/render-ctx)] + render-id (mf/use-ctx muc/render-id)] [:* [:g {:clip-path (when (not show-content) (frame-clip-url shape render-id))} diff --git a/frontend/src/app/main/ui/shapes/gradients.cljs b/frontend/src/app/main/ui/shapes/gradients.cljs index a57678bf4..9d45482f4 100644 --- a/frontend/src/app/main/ui/shapes/gradients.cljs +++ b/frontend/src/app/main/ui/shapes/gradients.cljs @@ -104,7 +104,7 @@ (let [attr (obj/get props "attr") shape (obj/get props "shape") id (obj/get props "id") - id' (mf/use-ctx muc/render-ctx) + id' (mf/use-ctx muc/render-id) id (or id (dm/str (name attr) "_" id')) gradient (get shape attr) gradient-props #js {:id id diff --git a/frontend/src/app/main/ui/shapes/group.cljs b/frontend/src/app/main/ui/shapes/group.cljs index 88dc6186d..664c4e834 100644 --- a/frontend/src/app/main/ui/shapes/group.cljs +++ b/frontend/src/app/main/ui/shapes/group.cljs @@ -21,7 +21,7 @@ (let [shape (unchecked-get props "shape") childs (unchecked-get props "childs") objects (unchecked-get props "objects") - render-id (mf/use-ctx muc/render-ctx) + render-id (mf/use-ctx muc/render-id) masked-group? (:masked-group? shape) [mask childs] (if masked-group? diff --git a/frontend/src/app/main/ui/shapes/mask.cljs b/frontend/src/app/main/ui/shapes/mask.cljs index 5c353d120..c43b12a60 100644 --- a/frontend/src/app/main/ui/shapes/mask.cljs +++ b/frontend/src/app/main/ui/shapes/mask.cljs @@ -47,18 +47,18 @@ {::mf/wrap-props false} [props] (let [mask (unchecked-get props "mask") - render-id (mf/use-ctx muc/render-ctx) + render-id (mf/use-ctx muc/render-id) svg-text? (and (= :text (:type mask)) (some? (:position-data mask))) ;; 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. + ;; 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-bb (-> (gsh/transform-shape mask) (cond-> fixed? (gsh/move delta)) (:points)) - + mask-bb-rect (gsh/points->rect mask-bb)] [:defs [:filter {:id (filter-id render-id mask)} diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index c7ea0ebf3..a457534a7 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -9,8 +9,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] - [app.common.uuid :as uuid] [app.main.ui.context :as muc] + [app.main.ui.hooks :as h] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.export :as ed] [app.main.ui.shapes.fills :as fills] @@ -49,18 +49,19 @@ {::mf/forward-ref true ::mf/wrap-props false} [props ref] - (let [shape (obj/get props "shape") - children (obj/get props "children") - pointer-events (obj/get props "pointer-events") - disable-shadows? (obj/get props "disable-shadows?") - type (:type shape) - render-id (mf/use-memo #(str (uuid/next))) - filter-id (str "filter_" render-id) - styles (-> (obj/create) - (obj/set! "pointerEvents" pointer-events) - (cond-> (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) - (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) + (let [shape (unchecked-get props "shape") + children (unchecked-get props "children") + pointer-events (unchecked-get props "pointer-events") + disable-shadows? (unchecked-get props "disable-shadows?") + + type (:type shape) + render-id (h/use-id) + filter-id (dm/str "filter_" render-id) + styles (-> (obj/create) + (obj/set! "pointerEvents" pointer-events) + (cond-> (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) + (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) include-metadata? (mf/use-ctx ed/include-metadata-ctx) @@ -91,7 +92,7 @@ svg-group? (propagate-wrapper-styles wrapper-props))] - [:& (mf/provider muc/render-ctx) {:value render-id} + [:& (mf/provider muc/render-id) {:value render-id} [:> :g wrapper-props (when include-metadata? [:& ed/export-data {:shape shape}]) 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 158b6c117..eb5908c68 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -9,7 +9,6 @@ [app.common.colors :as clr] [app.common.data :as d] [app.common.geom.shapes :as gsh] - [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.text.styles :as sts] [app.util.color :as uc] @@ -91,23 +90,6 @@ (recur (uc/next-rgb current-rgb)) current-hex)))) -(defn- remap-colors - "Returns a new content replacing the original colors by their mapped 'simple color'" - [content color-mapping] - - (cond-> content - (and (:fill-opacity content) (< (:fill-opacity content) 1.0)) - (-> (assoc :fill-color (get color-mapping [(:fill-color content) (:fill-opacity content)])) - (assoc :fill-opacity 1.0)) - - (some? (:fill-color-gradient content)) - (-> (assoc :fill-color (get color-mapping (:fill-color-gradient content))) - (assoc :fill-opacity 1.0) - (dissoc :fill-color-gradient)) - - (contains? content :children) - (update :children #(mapv (fn [node] (remap-colors node color-mapping)) %)))) - (defn- fill->color "Given a content node returns the information about that node fill color" [{:keys [fill-color fill-opacity fill-color-gradient]}] @@ -199,13 +181,7 @@ ;; We add 8px to add a padding for the exporter ;; width (+ width 8) - [colors color-mapping color-mapping-inverse] (retrieve-colors shape) - - plain-colors? (mf/use-ctx muc/text-plain-colors-ctx) - - content (cond-> content - plain-colors? - (remap-colors color-mapping))] + [colors _color-mapping color-mapping-inverse] (retrieve-colors shape)] [:foreignObject {:x x 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 b4585b532..3f90c6cd3 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -53,7 +53,7 @@ ::mf/wrap [mf/memo]} [props] - (let [render-id (mf/use-ctx muc/render-ctx) + (let [render-id (mf/use-ctx muc/render-id) shape (obj/get props "shape") shape (cond-> shape (:is-mask? shape) set-white-fill) @@ -106,6 +106,6 @@ (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))}) shape (assoc shape :fills (:fills data))] - [:& (mf/provider muc/render-ctx) {:key index :value (str render-id "_" (:id shape) "_" index)} + [:& (mf/provider muc/render-id) {:key index :value (str render-id "_" (:id shape) "_" index)} [:& shape-custom-strokes {:shape shape :position index :render-id render-id} [:> :text props (:text data)]]]))]])) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index f2f86ace9..ced7b3c6a 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -27,7 +27,7 @@ [app.main.ui.static :as static] [app.main.ui.viewer.comments :refer [comments-layer comments-sidebar]] [app.main.ui.viewer.handoff :as handoff] - [app.main.ui.viewer.header :refer [header]] + [app.main.ui.viewer.header :as header] [app.main.ui.viewer.interactions :as interactions] [app.main.ui.viewer.login] [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] @@ -36,8 +36,15 @@ [app.util.webapi :as wapi] [cuerdas.core :as str] [goog.events :as events] + [okulary.core :as l] [rumext.alpha :as mf])) +(def current-animation-ref + (l/derived :viewer-animation st/state)) + +(def current-overlays-ref + (l/derived :viewer-overlays st/state)) + (defn- calculate-size [objects frame zoom] (let [{:keys [x y width height]} (gsb/get-object-bounds objects frame)] @@ -60,7 +67,6 @@ :height (* height zoom) :vbox (str "0 0 " width " " height)}))) - (mf/defc viewer-pagination [{:keys [index num-frames left-bar right-bar] :as props}] [:* @@ -75,94 +81,132 @@ [:div.counter (str/join " / " [(+ index 1) num-frames])] [:span]]]) -(mf/defc viewer-wrapper - [{:keys [wrapper-size scroll orig-frame orig-viewport-ref orig-size page file users current-viewport-ref - size frame interactions-mode overlays zoom close-overlay section index] :as props}] - (let [{clist :list} (mf/deref refs/comments-local) - show-comments-list (and (= section :comments) (= :show clist))] +(mf/defc viewer-pagination-and-sidebar + {::mf/wrap [mf/memo]} + [{:keys [section index users frame page]}] + (let [comments-local (mf/deref refs/comments-local) + show-sidebar? (and (= section :comments) (:show-sidebar? comments-local))] [:* - [:& viewer-pagination {:index index :num-frames (count (:frames page)) :right-bar show-comments-list}] + [:& viewer-pagination + {:index index + :num-frames (count (:frames page)) + :right-bar show-sidebar?}] - (when show-comments-list - [:& comments-sidebar {:users users :frame frame :page page}]) + (when show-sidebar? + [:& comments-sidebar + {:users users + :frame frame + :page page}])])) - [:div.viewer-wrapper - {:style {:width (:width wrapper-size) - :height (:height wrapper-size)}} - [:& (mf/provider ctx/scroll-ctx) {:value @scroll} - [:div.viewer-clipper - [:* - (when orig-frame - [:div.viewport-container - {:ref orig-viewport-ref - :style {:width (:width orig-size) - :height (:height orig-size) - :position "relative"}} +(mf/defc viewer-overlay + [{:keys [overlay page frame zoom wrapper-size close-overlay interactions-mode]}] + (let [close-click-outside? (:close-click-outside overlay) + background-overlay? (:background-overlay overlay) + overlay-frame (:frame overlay) + overlay-position (:position overlay) - [:& interactions/viewport - {:frame orig-frame - :base-frame orig-frame - :frame-offset (gpt/point 0 0) - :size orig-size - :page page - :file file - :users users - :interactions-mode :hide}]]) + size + (mf/with-memo [page overlay zoom] + (calculate-size (:objects page) (:frame overlay) zoom)) - [:div.viewport-container - {:ref current-viewport-ref - :style {:width (:width size) - :height (:height size) - :position "relative"}} + on-click + (mf/use-fn + (mf/deps overlay close-overlay close-click-outside?) + (fn [_] + (when close-click-outside? + (close-overlay (:frame overlay)))))] - [:& interactions/viewport - {:frame frame - :base-frame frame - :frame-offset (gpt/point 0 0) - :size size - :page page - :file file - :users users - :interactions-mode interactions-mode}] + [:* + (when (or close-click-outside? background-overlay?) + [:div.viewer-overlay-background + {:class (dom/classnames :visible background-overlay?) + :style {:width (:width wrapper-size) + :height (:height wrapper-size) + :position "absolute" + :left 0 + :top 0} + :on-click on-click}]) - (for [overlay overlays] - (let [size-over (calculate-size (:objects page) (:frame overlay) zoom)] - [:* - (when (or (:close-click-outside overlay) - (:background-overlay overlay)) - [:div.viewer-overlay-background - {:class (dom/classnames - :visible (:background-overlay overlay)) - :style {:width (:width wrapper-size) - :height (:height wrapper-size) - :position "absolute" - :left 0 - :top 0} - :on-click #(when (:close-click-outside overlay) - (close-overlay (:frame overlay)))}]) - [:div.viewport-container.viewer-overlay + [:div.viewport-container.viewer-overlay + {:id (dm/str "overlay-" (:id overlay-frame)) + :style {:width (:width size) + :height (:height size) + :left (* (:x overlay-position) zoom) + :top (* (:y overlay-position) zoom)}} - {:id (str "overlay-" (-> overlay :frame :id)) - :style {:width (:width size-over) - :height (:height size-over) - :left (* (:x (:position overlay)) zoom) - :top (* (:y (:position overlay)) zoom)}} - [:& interactions/viewport - {:frame (:frame overlay) - :base-frame frame - :frame-offset (:position overlay) - :size size-over - :page page - :file file - :users users - :interactions-mode interactions-mode}]]]))]] + [:& interactions/viewport + {:frame overlay-frame + :base-frame frame + :frame-offset overlay-position + :size size + :page page + :interactions-mode interactions-mode}]]])) - (when (= section :comments) - [:& comments-layer {:file file - :users users - :frame frame - :page page - :zoom zoom}])]]]])) + +(mf/defc viewer-wrapper + [{:keys [wrapper-size orig-frame orig-viewport-ref orig-size page file users current-viewport-ref + size frame interactions-mode overlays zoom close-overlay section index] :as props}] + [:* + [:& viewer-pagination-and-sidebar + {:section section + :index index + :page page + :users users + :frame frame}] + + [:div.viewer-wrapper + {:style {:width (:width wrapper-size) + :height (:height wrapper-size)}} + [:div.viewer-clipper + (when orig-frame + [:div.viewport-container + {:ref orig-viewport-ref + :style {:width (:width orig-size) + :height (:height orig-size) + :position "relative"}} + + [:& interactions/viewport + {:frame orig-frame + :base-frame orig-frame + :frame-offset (gpt/point 0 0) + :size orig-size + :page page + :users users + :interactions-mode :hide}]]) + + [:div.viewport-container + {:ref current-viewport-ref + :style {:width (:width size) + :height (:height size) + :position "relative"}} + + [:& interactions/viewport + {:frame frame + :base-frame frame + :frame-offset (gpt/point 0 0) + :size size + :page page + :interactions-mode interactions-mode}] + + (for [overlay overlays] + [:& viewer-overlay {:overlay overlay + :key (dm/str (:id overlay)) + :page page + :frame frame + :zoom zoom + :wrapper-size wrapper-size + :close-overlay close-overlay + :interactions-mode interactions-mode}]) + + ]] + + + (when (= section :comments) + [:& comments-layer {:file file + :users users + :frame frame + :page page + :zoom zoom}])]]) (mf/defc viewer [{:keys [params data]}] @@ -184,10 +228,11 @@ local (mf/deref refs/viewer-local) nav-scroll (:nav-scroll local) - orig-viewport-ref (mf/use-ref nil) + orig-viewport-ref (mf/use-ref nil) current-viewport-ref (mf/use-ref nil) - viewer-section-ref (mf/use-ref nil) - current-animation (:current-animation local) + viewer-section-ref (mf/use-ref nil) + + current-animation (mf/deref current-animation-ref) page-id (or page-id (-> file :data :pages first)) @@ -206,47 +251,48 @@ frames (:frames page) frame (get frames index) - fullscreen? (mf/deref refs/viewer-fullscreen?) - overlays (:overlays local) - scroll (mf/use-state nil) + fullscreen? (mf/deref header/fullscreen-ref) + overlays (mf/deref current-overlays-ref) + scroll (mf/use-state nil) orig-frame (when (:orig-frame-id current-animation) (d/seek #(= (:id %) (:orig-frame-id current-animation)) frames)) - size (mf/use-memo - (mf/deps frame zoom) - (fn [] (calculate-size (:objects page) frame zoom))) + size + (mf/with-memo [frame zoom] + (calculate-size (:objects page) frame zoom)) - orig-size (mf/use-memo - (mf/deps orig-frame zoom) - (fn [] (when orig-frame - (calculate-size (:objects page) orig-frame zoom)))) + orig-size + (mf/with-memo [orig-frame zoom] + (when orig-frame + (calculate-size (:objects page) orig-frame zoom))) - wrapper-size (mf/use-memo - (mf/deps size orig-size zoom) - (fn [] (calculate-wrapper size orig-size zoom))) + wrapper-size + (mf/with-memo [size orig-size zoom] + (calculate-wrapper size orig-size zoom)) interactions-mode (:interactions-mode local) on-click - (mf/use-callback + (mf/use-fn (mf/deps section) (fn [_] (when (= section :comments) (st/emit! (dcm/close-thread))))) set-up-new-size - (mf/use-callback + (mf/use-fn (fn [_] (let [viewer-section (dom/get-element "viewer-section") size (dom/get-client-size viewer-section)] (st/emit! (dv/set-viewport-size {:size size}))))) on-scroll - (fn [event] - (reset! scroll (dom/get-target-scroll event)))] + (mf/use-fn + (fn [event] + (reset! scroll (dom/get-target-scroll event))))] (hooks/use-shortcuts ::viewer sc/shortcuts) @@ -264,14 +310,13 @@ (let [name (:name file)] (dom/set-html-title (str "\u25b6 " (tr "title.viewer" name)))))) - (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 [] - (events/unlistenByKey key1) - (events/unlistenByKey key2))))) + (mf/with-effect [] + (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 [] + (events/unlistenByKey key1) + (events/unlistenByKey key2)))) (mf/use-layout-effect (fn [] @@ -362,20 +407,22 @@ fonts (into #{} (keep :font-id) text-nodes)] (run! fonts/ensure-loaded! fonts)))) - [:div#viewer-layout {:class (dom/classnames - :force-visible (:show-thumbnails local) - :viewer-layout (not= section :handoff) - :handoff-layout (= section :handoff) - :fullscreen fullscreen?)} + [:div#viewer-layout + {:class (dom/classnames + :force-visible (:show-thumbnails local) + :viewer-layout (not= section :handoff) + :handoff-layout (= section :handoff) + :fullscreen fullscreen?)} - [:& header {:project project - :index index - :file file - :page page - :frame frame - :permissions permissions - :zoom zoom - :section section}] + [:& header/header + {:project project + :index index + :file file + :page page + :frame frame + :permissions permissions + :zoom zoom + :section section}] [:div.viewer-content [:div.thumbnail-close {:on-click #(st/emit! dv/close-thumbnails-panel) @@ -410,23 +457,24 @@ :index index :viewer-pagination viewer-pagination}] - [:& viewer-wrapper - {:wrapper-size wrapper-size - :scroll scroll - :orig-frame orig-frame - :orig-viewport-ref orig-viewport-ref - :orig-size orig-size - :page page - :file file - :users users - :current-viewport-ref current-viewport-ref - :size size - :frame frame - :interactions-mode interactions-mode - :overlays overlays - :zoom zoom - :section section - :index index}]))]]])) + [:& (mf/provider ctx/current-scroll) {:value @scroll} + [:& (mf/provider ctx/current-zoom) {:value zoom} + [:& viewer-wrapper + {:wrapper-size wrapper-size + :orig-frame orig-frame + :orig-viewport-ref orig-viewport-ref + :orig-size orig-size + :page page + :file file + :users users + :current-viewport-ref current-viewport-ref + :size size + :frame frame + :interactions-mode interactions-mode + :overlays overlays + :zoom zoom + :section section + :index index}]]]))]]])) ;; --- Component: Viewer Page diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs index c46147ed9..96c44b262 100644 --- a/frontend/src/app/main/ui/viewer/comments.cljs +++ b/frontend/src/app/main/ui/viewer/comments.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.viewer.comments (:require + [app.common.data :as d] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -23,132 +24,145 @@ [rumext.alpha :as mf])) (mf/defc comments-menu + {::mf/wrap [mf/memo] + ::mf/wrap-props false} [] - (let [{cmode :mode cshow :show clist :list} (mf/deref refs/comments-local) + (let [local (mf/deref refs/comments-local) + owner-filter (:owner-filter local) + status-filter (:status-filter local) + show-sidebar? (:show-sidebar? local) show-dropdown? (mf/use-state false) toggle-dropdown (mf/use-fn #(swap! show-dropdown? not)) hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) - update-mode - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:mode mode})))) - - update-show - (mf/use-callback - (fn [mode] - (st/emit! (dcm/update-filters {:show mode})))) - - update-list - (mf/use-callback - (fn [show-list] - (st/emit! (dcm/update-filters {:list show-list}))))] + update-option (mf/use-fn + (fn [event] + (let [target (dom/get-current-target event) + key (d/read-string (dom/get-attribute target "data-key")) + val (d/read-string (dom/get-attribute target "data-val"))] + (st/emit! (dcm/update-options {key val})))))] [:div.view-options {:on-click toggle-dropdown} [:span.label (tr "labels.comments")] [:span.icon i/arrow-down] [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} + [:ul.dropdown.with-check - [:li {:class (dom/classnames :selected (= :all cmode)) - :on-click #(update-mode :all)} + [:li {:class (dom/classnames :selected (= :all owner-filter)) + :data-key ":owner-filter" + :data-val ":all" + :on-click update-option} [:span.icon i/tick] [:span.label (tr "labels.show-all-comments")]] - [:li {:class (dom/classnames :selected (= :yours cmode)) - :on-click #(update-mode :yours)} + [:li {:class (dom/classnames :selected (= :yours owner-filter)) + :data-key ":owner-filter" + :data-val ":yours" + :on-click update-option} [:span.icon i/tick] [:span.label (tr "labels.show-your-comments")]] [:hr] - [:li {:class (dom/classnames :selected (= :pending cshow)) - :on-click #(update-show (if (= :pending cshow) :all :pending))} + [:li {:class (dom/classnames :selected (= :pending status-filter)) + :data-key ":status-filter" + :data-val (if (= :pending status-filter) ":all" ":pending") + :on-click update-option} [:span.icon i/tick] [:span.label (tr "labels.hide-resolved-comments")]] [:hr] - - [:li {:class (dom/classnames :selected (= :show clist)) - :on-click #(update-list (if (= :show clist) :hide :show))} + [:li {:class (dom/classnames :selected show-sidebar?) + :data-key ":show-sidebar?" + :data-val (if show-sidebar? "false" "true") + :on-click update-option} [:span.icon i/tick] [:span.label (tr "labels.show-comments-list")]]]]])) -(def threads-ref - (l/derived :comment-threads st/state)) - -(def comments-local-ref - (l/derived :comments-local st/state)) +(defn- update-thread-position [positions {:keys [id] :as thread}] + (if-let [data (get positions id)] + (-> thread + (assoc :position (:position data)) + (assoc :frame-id (:frame-id data))) + thread)) (mf/defc comments-layer [{:keys [zoom file users frame page] :as props}] - (let [profile (mf/deref refs/profile) - threads-position-ref (l/derived (l/in [:viewer :pages (:id page) :options :comment-threads-position]) st/state) - threads-position-map (mf/deref threads-position-ref) - threads-map (mf/deref threads-ref) + (prn "comments-layer") + (let [profile (mf/deref refs/profile) + local (mf/deref refs/comments-local) - frame-corner (-> frame :points gsh/points->selrect gpt/point) - modifier1 (-> (gmt/matrix) - (gmt/translate (gpt/negate frame-corner))) + open-thread-id (:open local) + page-id (:id page) + file-id (:id file) + frame-id (:id frame) - modifier2 (-> (gpt/point frame-corner) - (gmt/translate-matrix)) + tpos-ref (mf/with-memo [page-id] + (-> (l/in [:pages page-id :options :comment-threads-position]) + (l/derived refs/viewer-data))) - cstate (mf/deref refs/comments-local) + positions (mf/deref tpos-ref) + threads-map (mf/deref refs/comment-threads) - update-thread-position (fn update-thread-position [thread] - (if (contains? threads-position-map (:id thread)) - (-> thread - (assoc :position (get-in threads-position-map [(:id thread) :position])) - (assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id]))) - thread)) + frame-corner (mf/with-memo [frame] + (-> frame :points gsh/points->selrect gpt/point)) + + modifier1 (mf/with-memo [frame-corner] + (-> (gmt/matrix) + (gmt/translate (gpt/negate frame-corner)))) + modifier2 (mf/with-memo [frame-corner] + (-> (gpt/point frame-corner) + (gmt/translate-matrix))) + + + threads (mf/with-memo [threads-map positions] + (->> (vals threads-map) + (map (partial update-thread-position positions)) + (filter #(= (:frame-id %) (:id frame))) + (dcm/apply-filters local profile) + (filter (fn [{:keys [position]}] + (gsh/has-point? frame position))))) - threads (->> (vals threads-map) - (map update-thread-position) - (filter #(= (:frame-id %) (:id frame))) - (dcm/apply-filters cstate profile) - (filter (fn [{:keys [position]}] - (gsh/has-point? frame position)))) on-bubble-click - (mf/use-callback - (mf/deps cstate) - (fn [thread] - (if (= (:open cstate) (:id thread)) - (st/emit! (dcm/close-thread)) - (st/emit! (-> (dcm/open-thread thread) + (mf/use-fn + (mf/deps open-thread-id) + (fn [{:keys [id] :as thread}] + (st/emit! (if (= open-thread-id id) + (dcm/close-thread) + (-> (dcm/open-thread thread) (with-meta {::ev/origin "viewer"})))))) on-click - (mf/use-callback - (mf/deps cstate frame page file zoom) + (mf/use-fn + (mf/deps open-thread-id zoom page-id file-id modifier2) (fn [event] (dom/stop-propagation event) - (if (some? (:open cstate)) + (if (some? open-thread-id) (st/emit! (dcm/close-thread)) - (let [event (.-nativeEvent ^js event) - viewport-point (dom/get-offset-position event) - viewport-point (-> viewport-point (update :x #(/ % zoom)) (update :y #(/ % zoom))) - position (gpt/transform viewport-point modifier2) + (let [event (dom/event->native-event event) + position (-> (dom/get-offset-position event) + (update :x #(/ % zoom)) + (update :y #(/ % zoom)) + (gpt/transform modifier2)) params {:position position :page-id (:id page) :file-id (:id file)}] (st/emit! (dcm/create-draft params)))))) on-draft-cancel - (mf/use-callback - (mf/deps cstate) - #(st/emit! (dcm/close-thread))) + (mf/use-fn #(st/emit! (dcm/close-thread))) on-draft-submit - (mf/use-callback - (mf/deps frame) + (mf/use-fn + (mf/deps frame-id modifier2) (fn [draft] (let [params (-> draft - (update :position gpt/transform modifier2) - (assoc :frame-id (:id frame)))] + (update :position gpt/transform modifier2) + (assoc :frame-id frame-id))] (st/emit! (dcm/create-thread-on-viewer params) (dcm/close-thread)))))] @@ -156,35 +170,37 @@ [:div.viewer-comments-container [:div.threads (for [item threads] - (let [item (update item :position gpt/transform modifier1)] - [:& cmt/thread-bubble {:thread item - :zoom zoom - :on-click on-bubble-click - :open? (= (:id item) (:open cstate)) - :key (:seqn item) - :origin :viewer}])) + [:& cmt/thread-bubble + {:thread item + :position-modifier modifier1 + :zoom zoom + :on-click on-bubble-click + :open? (= (:id item) (:open local)) + :key (:seqn item) + :origin :viewer}]) - (when-let [id (:open cstate)] - (when-let [thread (as-> (get threads-map id) $ - (when (some? $) - (update $ :position gpt/transform modifier1)))] - [:& cmt/thread-comments {:thread thread - :users users - :zoom zoom}])) + (when-let [thread (get threads-map open-thread-id)] + [:& cmt/thread-comments + {:thread thread + :position-modifier modifier1 + :users users + :zoom zoom}]) - (when-let [draft (:draft cstate)] - [:& cmt/draft-thread {:draft (update draft :position gpt/transform modifier1) - :on-cancel on-draft-cancel - :on-submit on-draft-submit - :zoom zoom}])]]])) + (when-let [draft (:draft local)] + [:& cmt/draft-thread + {:draft draft + :position-modifier modifier1 + :on-cancel on-draft-cancel + :on-submit on-draft-submit + :zoom zoom}])]]])) (mf/defc comments-sidebar [{:keys [users frame page]}] (let [profile (mf/deref refs/profile) - cstate (mf/deref refs/comments-local) - threads-map (mf/deref threads-ref) + local (mf/deref refs/comments-local) + threads-map (mf/deref refs/comment-threads) threads (->> (vals threads-map) - (dcm/apply-filters cstate profile) + (dcm/apply-filters local profile) (filter (fn [{:keys [position]}] (gsh/has-point? frame position))))] [:aside.settings-bar.settings-bar-right.comments-right-sidebar diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs index 8a238fea6..2eb108de6 100644 --- a/frontend/src/app/main/ui/viewer/handoff.cljs +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -6,13 +6,13 @@ (ns app.main.ui.viewer.handoff (:require - [app.main.data.viewer :as dv] + [app.main.data.viewer :as dv] [app.main.store :as st] [app.main.ui.viewer.handoff.left-sidebar :refer [left-sidebar]] [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] [app.main.ui.viewer.handoff.right-sidebar :refer [right-sidebar]] [app.util.dom :as dom] - [app.util.keyboard :as kbd] + [app.util.keyboard :as kbd] [goog.events :as events] [rumext.alpha :as mf]) (:import goog.events.EventType)) diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs index 3cfb179bf..61d982910 100644 --- a/frontend/src/app/main/ui/viewer/handoff/render.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -188,9 +188,8 @@ (mf/defc render-frame-svg [{:keys [page frame local size]}] - (let [objects (mf/use-memo - (mf/deps page frame) - (prepare-objects page frame size)) + (let [objects (mf/with-memo [page frame size] + (prepare-objects page frame size)) ;; Retrieve frame again with correct modifier frame (get objects (:id frame)) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 034473649..2fbf99e51 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -6,10 +6,10 @@ (ns app.main.ui.viewer.header (:require + [app.common.data.macros :as dm] [app.main.data.modal :as modal] [app.main.data.viewer :as dv] [app.main.data.viewer.shortcuts :as sc] - [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.export :refer [export-progress-widget]] @@ -19,8 +19,14 @@ [app.main.ui.viewer.interactions :refer [flows-menu interactions-menu]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] + [okulary.core :as l] [rumext.alpha :as mf])) +(def fullscreen-ref + (l/derived (fn [state] + (dm/get-in state [:viewer-local :fullscreen?])) + st/state)) + (defn open-login-dialog [] (modal/show! :login-register {})) @@ -65,7 +71,7 @@ (mf/defc header-options [{:keys [section zoom page file index permissions]}] - (let [fullscreen? (mf/deref refs/viewer-fullscreen?) + (let [fullscreen? (mf/deref fullscreen-ref) toggle-fullscreen (mf/use-callback diff --git a/frontend/src/app/main/ui/viewer/interactions.cljs b/frontend/src/app/main/ui/viewer/interactions.cljs index 32c2a0c0d..aa479c9cb 100644 --- a/frontend/src/app/main/ui/viewer/interactions.cljs +++ b/frontend/src/app/main/ui/viewer/interactions.cljs @@ -17,6 +17,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.viewer.shapes :as shapes] [app.util.dom :as dom] @@ -27,84 +28,109 @@ (defn prepare-objects [page frame size] - (fn [] - (let [objects (:objects page) - frame-id (:id frame) - modifier (-> (gpt/point (:x size) (:y size)) - (gpt/negate) - (gmt/translate-matrix)) + (let [objects (:objects page) + frame-id (:id frame) + modifier (-> (gpt/point (:x size) (:y size)) + (gpt/negate) + (gmt/translate-matrix)) - update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)] + update-fn #(d/update-when %1 %2 assoc-in [:modifiers :displacement] modifier)] - (->> (cph/get-children-ids objects frame-id) - (into [frame-id]) - (reduce update-fn objects))))) + (->> (cph/get-children-ids objects frame-id) + (into [frame-id]) + (reduce update-fn objects)))) -(mf/defc viewport - {::mf/wrap [mf/memo]} - [{:keys [page interactions-mode frame base-frame frame-offset size]}] - (let [objects (mf/use-memo - (mf/deps page frame size) - (prepare-objects page frame size)) +(mf/defc viewport-svg + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [props] + (let [page (unchecked-get props "page") + frame (unchecked-get props "frame") + base (unchecked-get props "base") + offset (unchecked-get props "offset") + size (unchecked-get props "size") - wrapper (mf/use-memo - (mf/deps objects) - #(shapes/frame-container-factory objects)) + vbox (:vbox size) + + objects (mf/with-memo [page frame size] + (prepare-objects page frame size)) + + wrapper (mf/with-memo [objects] + (shapes/frame-container-factory objects)) ;; Retrieve frames again with correct modifier - frame (get objects (:id frame)) - base-frame (get objects (:id base-frame)) + frame (get objects (:id frame)) + base (get objects (:id base))] - on-click - (fn [_] - (when (= interactions-mode :show-on-click) - (st/emit! dv/flash-interactions))) - - on-mouse-wheel - (fn [event] - (when (kbd/mod? event) - (dom/prevent-default event) - (let [event (.getBrowserEvent ^js event) - delta (+ (.-deltaY ^js event) (.-deltaX ^js event))] - (if (pos? delta) - (st/emit! dv/decrease-zoom) - (st/emit! dv/increase-zoom))))) - - on-key-down - (fn [event] - (when (kbd/esc? event) - (st/emit! (dcm/close-thread))))] - - (mf/use-effect - (mf/deps interactions-mode) ;; on-click event depends on interactions-mode - (fn [] - ;; bind with passive=false to allow the event to be cancelled - ;; https://stackoverflow.com/a/57582286/3219895 - (let [key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false}) - key2 (events/listen js/window "keydown" on-key-down) - key3 (events/listen js/window "click" on-click)] - (fn [] - (events/unlistenByKey key1) - (events/unlistenByKey key2) - (events/unlistenByKey key3))))) - - [:& (mf/provider shapes/base-frame-ctx) {:value base-frame} - [:& (mf/provider shapes/frame-offset-ctx) {:value frame-offset} - [:svg {:view-box (:vbox size) + [:& (mf/provider shapes/base-frame-ctx) {:value base} + [:& (mf/provider shapes/frame-offset-ctx) {:value offset} + [:svg {:view-box vbox :width (:width size) :height (:height size) :version "1.1" :xmlnsXlink "http://www.w3.org/1999/xlink" :xmlns "http://www.w3.org/2000/svg" :fill "none"} - [:& wrapper {:shape frame - :view-box (:vbox size)}]]]])) + [:& wrapper {:shape frame :view-box vbox}]]]])) +(mf/defc viewport + {::mf/wrap [mf/memo] + ::mf/wrap-props false} + [props] + (let [;; NOTE: with `use-equal-memo` hook we ensure that all values + ;; conserves the reference identity for avoid unnecesary dummy + ;; rerenders. + mode (h/use-equal-memo (unchecked-get props "interactions-mode")) + offset (h/use-equal-memo (unchecked-get props "frame-offset")) + size (h/use-equal-memo (unchecked-get props "size")) + + page (unchecked-get props "page") + frame (unchecked-get props "frame") + base (unchecked-get props "base-frame")] + + (mf/with-effect [mode] + (let [on-click + (fn [_] + (when (= mode :show-on-click) + (st/emit! (dv/flash-interactions)))) + + on-mouse-wheel + (fn [event] + (when (kbd/mod? event) + (dom/prevent-default event) + (let [event (dom/event->browser-event event) + delta (+ (.-deltaY ^js event) + (.-deltaX ^js event))] + (if (pos? delta) + (st/emit! dv/decrease-zoom) + (st/emit! dv/increase-zoom))))) + + on-key-down + (fn [event] + (when (kbd/esc? event) + (st/emit! (dcm/close-thread)))) + + + ;; bind with passive=false to allow the event to be cancelled + ;; https://stackoverflow.com/a/57582286/3219895 + key1 (events/listen goog/global "wheel" on-mouse-wheel #js {"passive" false}) + key2 (events/listen goog/global "keydown" on-key-down) + key3 (events/listen goog/global "click" on-click)] + (fn [] + (events/unlistenByKey key1) + (events/unlistenByKey key2) + (events/unlistenByKey key3)))) + + [:& viewport-svg {:page page + :frame frame + :base base + :offset offset + :size size}])) (mf/defc flows-menu {::mf/wrap [mf/memo]} [{:keys [page index]}] - (let [flows (get-in page [:options :flows]) + (let [flows (dm/get-in page [:options :flows]) frames (:frames page) frame (get frames index) current-flow (mf/use-state @@ -135,7 +161,6 @@ [:span.icon i/tick] [:span.label (:name flow)]])]]]))) - (mf/defc interactions-menu [] (let [local (mf/deref refs/viewer-local) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 2ffe4f186..ae280e215 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -206,19 +206,22 @@ :style {:pointer-events (when frame? "none")} :transform (gsh/transform-str shape)}]))) + +;; TODO: use-memo use-fn + (defn generic-wrapper-factory "Wrap some svg shape and add interaction controls" [component] (mf/fnc generic-wrapper {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - childs (unchecked-get props "childs") - frame (unchecked-get props "frame") - objects (unchecked-get props "objects") - fixed? (unchecked-get props "fixed?") - delta (unchecked-get props "delta") - base-frame (mf/use-ctx base-frame-ctx) + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + frame (unchecked-get props "frame") + objects (unchecked-get props "objects") + fixed? (unchecked-get props "fixed?") + delta (unchecked-get props "delta") + base-frame (mf/use-ctx base-frame-ctx) frame-offset (mf/use-ctx frame-offset-ctx) interactions-show? (mf/deref viewer-interactions-show?) @@ -226,20 +229,37 @@ interactions (:interactions shape) svg-element? (and (= :svg-raw (:type shape)) - (not= :svg (get-in shape [:content :tag])))] + (not= :svg (get-in shape [:content :tag]))) - (mf/use-effect - (fn [] - (let [sems (on-load shape base-frame frame-offset objects)] - #(run! tm/dispose! sems)))) + + on-mouse-down + (mf/use-fn (mf/deps shape base-frame frame-offset objects) + #(on-mouse-down % shape base-frame frame-offset objects)) + + on-mouse-up + (mf/use-fn (mf/deps shape base-frame frame-offset objects) + #(on-mouse-up % shape base-frame frame-offset objects)) + + on-mouse-enter + (mf/use-fn (mf/deps shape base-frame frame-offset objects) + #(on-mouse-enter % shape base-frame frame-offset objects)) + + on-mouse-leave + (mf/use-fn (mf/deps shape base-frame frame-offset objects) + #(on-mouse-leave % shape base-frame frame-offset objects))] + + + (mf/with-effect [] + (let [sems (on-load shape base-frame frame-offset objects)] + (partial run! tm/dispose! sems))) (if-not svg-element? [:> shape-container {:shape shape :cursor (when (ctsi/actionable? interactions) "pointer") - :on-mouse-down #(on-mouse-down % shape base-frame frame-offset objects) - :on-mouse-up #(on-mouse-up % shape base-frame frame-offset objects) - :on-mouse-enter #(on-mouse-enter % shape base-frame frame-offset objects) - :on-mouse-leave #(on-mouse-leave % shape base-frame frame-offset objects)} + :on-mouse-down on-mouse-down + :on-mouse-up on-mouse-up + :on-mouse-enter on-mouse-enter + :on-mouse-leave on-mouse-leave} [:& component {:shape shape :frame frame @@ -311,6 +331,7 @@ #js {:shape shape :childs childs :objects objects})] + [:> frame-wrapper props])))) (defn group-container-factory @@ -362,31 +383,43 @@ image-wrapper (image-wrapper) circle-wrapper (circle-wrapper)] (mf/fnc shape-container - {::mf/wrap-props false} + {::mf/wrap-props false + ::mf/wrap [mf/memo]} [props] - (let [scroll (mf/use-ctx ctx/scroll-ctx) - local (mf/deref refs/viewer-local) - zoom (:zoom local) - shape (unchecked-get props "shape") - parents (map (d/getf objects) (cph/get-parent-ids objects (:id shape))) - fixed? (or (:fixed-scroll shape) (some :fixed-scroll parents)) + (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") - delta {:x (/ (:scroll-left scroll) zoom) :y (/ (:scroll-top scroll) zoom)} + + ;; TODO: this watch of scroll position is killing + ;; performance of the viewer. + scroll (mf/use-ctx ctx/current-scroll) + zoom (mf/use-ctx ctx/current-zoom) + + fixed? (mf/with-memo [shape objects] + (->> (cph/get-parent-ids objects (:id shape)) + (map (d/getf objects)) + (concat [shape]) + (some :fixed-scroll))) + + delta {:x (/ (:scroll-left scroll) zoom) + :y (/ (:scroll-top scroll) zoom)} + group-container - (mf/use-memo (mf/deps objects) - #(group-container-factory objects)) + (mf/with-memo [objects] + (group-container-factory objects)) frame-container - (mf/use-memo (mf/deps objects) - #(frame-container-factory objects)) + (mf/with-memo [objects] + (frame-container-factory objects)) bool-container - (mf/use-memo (mf/deps objects) - #(bool-container-factory objects)) + (mf/with-memo [objects] + (bool-container-factory objects)) svg-raw-container - (mf/use-memo (mf/deps objects) - #(svg-raw-container-factory objects))] + (mf/with-memo [objects] + (svg-raw-container-factory objects)) + + ] (when (and shape (not (:hidden shape))) (let [shape (-> (gsh/transform-shape shape) (gsh/translate-to-frame frame) @@ -404,4 +437,3 @@ :group [:> group-container {:shape shape :frame frame :objects objects :fixed? fixed? :delta delta}] :bool [:> bool-container {:shape shape :frame frame :objects objects}] :svg-raw [:> svg-raw-container {:shape shape :frame frame :objects objects}]))))))) - diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 11003df3a..54a440d12 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -8,6 +8,7 @@ "A workspace specific context menu (mouse right click)." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.types.page :as ctp] [app.main.data.events :as ev] @@ -123,17 +124,25 @@ [:& menu-separator]])) (mf/defc context-menu-layer-position - [{:keys [hover-objs shapes]}] - (let [do-bring-forward #(st/emit! (dw/vertical-order-selected :up)) - do-bring-to-front #(st/emit! (dw/vertical-order-selected :top)) - do-send-backward #(st/emit! (dw/vertical-order-selected :down)) - do-send-to-back #(st/emit! (dw/vertical-order-selected :bottom)) - select-shapes (fn [id] #(st/emit! (dws/select-shape id)))] + [{:keys [shapes]}] + (let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up))) + do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top))) + do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down))) + do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom))) + select-shapes (fn [id] #(st/emit! (dws/select-shape id))) + + ;; NOTE: we use deref instead of mf/deref on objects because + ;; we really don't want rerender on object changes + hover-ids (deref refs/current-hover-ids) + objects (deref refs/workspace-page-objects) + hover-objs (into [] (keep (d/getf objects)) hover-ids)] + [:* (when (> (count hover-objs) 1) [:& menu-entry {:title (tr "workspace.shape.menu.select-layer")} (for [object hover-objs] [:& menu-entry {:title (:name object) + :key (dm/str (:id object)) :selected? (some #(= object %) shapes) :on-click (select-shapes (:id object)) :icon (si/element-icon {:shape object})}])]) @@ -435,14 +444,11 @@ :on-click do-delete}])) (mf/defc shape-context-menu + {::mf/wrap [mf/memo]} [{:keys [mdata] :as props}] (let [{:keys [disable-booleans? disable-flatten?]} mdata shapes (mf/deref refs/selected-objects) - hover-ids (mf/deref refs/current-hover-ids) - hover-objs (mf/deref (refs/objects-by-id hover-ids)) - props #js {:shapes shapes - :hover-objs hover-objs :disable-booleans? disable-booleans? :disable-flatten? disable-flatten?}] (when-not (empty? shapes) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 293492c83..0c93d33ed 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -54,7 +54,7 @@ (mf/deps objects) #(cph/objects-by-frame objects))] - [:& (mf/provider ctx/active-frames-ctx) {:value active-frames} + [:& (mf/provider ctx/active-frames) {:value active-frames} ;; Render font faces only for shapes that are part of the root ;; frame but don't belongs to any other frame. (let [xform (comp @@ -79,7 +79,7 @@ (let [shape (obj/get props "shape") active-frames - (when (cph/root-frame? shape) (mf/use-ctx ctx/active-frames-ctx)) + (when (cph/root-frame? shape) (mf/use-ctx ctx/active-frames)) thumbnail? (and (some? active-frames) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 89f669623..f2a5da6ba 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -37,6 +37,14 @@ (when (some? e) (.-target e))) +(defn event->native-event + [^js e] + (.-nativeEvent e)) + +(defn event->browser-event + [^js e] + (.getBrowserEvent e)) + ;; --- New methods (declare get-elements-by-tag)