diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index b642642ebb..928e87da4d 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -12,6 +12,7 @@ [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk] + [linked.set :as lks] [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.geom.shapes :as geom] @@ -133,13 +134,18 @@ ptk/WatchEvent (watch [_ state stream] (let [page-id (:current-page-id state) - selrect (get-in state [:workspace-local :selrect])] + selrect (get-in state [:workspace-local :selrect]) + is-not-blocked (fn [shape-id] (not (get-in state [:workspace-data + :pages-index page-id + :objects shape-id + :blocked] false)))] (rx/merge (rx/of (update-selrect nil)) (when selrect (->> (uw/ask! {:cmd :selection/query :page-id page-id :rect selrect}) + (rx/map #(into lks/empty-linked-set (filter is-not-blocked) %)) (rx/map select-shapes)))))))) (defn select-inside-group diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 54491833d3..5f9025dcce 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -257,9 +257,11 @@ (rx/switch-map #(snap/closest-snap-move page-id shapes objects layout %)) (rx/map #(gpt/round % 0)) (rx/map gmt/translate-matrix) - (rx/map #(set-modifiers ids {:displacement %}))) + (rx/map #(fn [state] (assoc-in state [:workspace-local :modifiers] {:displacement %})))) - (rx/of (apply-modifiers ids) + (rx/of (set-modifiers ids) + (apply-modifiers ids) + (fn [state] (update state :workspace-local dissoc :modifiers)) finish-transform))))))) (defn- get-displacement-with-grid @@ -339,13 +341,15 @@ ;; -- Apply modifiers (defn set-modifiers + ([ids] (set-modifiers ids nil true)) ([ids modifiers] (set-modifiers ids modifiers true)) ([ids modifiers recurse-frames?] (us/verify (s/coll-of uuid?) ids) (ptk/reify ::set-modifiers ptk/UpdateEvent (update [_ state] - (let [page-id (:current-page-id state) + (let [modifiers (or modifiers (get-in state [:workspace-local :modifiers] {})) + page-id (:current-page-id state) objects (dwc/lookup-page-objects state page-id) not-frame-id? diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index d16bed0102..f56bb31e83 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -49,6 +49,10 @@ (def selected-shapes (l/derived :selected workspace-local)) +(defn make-selected-ref + [id] + (l/derived #(contains? % id) selected-shapes)) + (def selected-zoom (l/derived :zoom workspace-local)) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 880691aef1..69e8fc5fee 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -11,6 +11,7 @@ "A workspace specific shapes wrappers." (:require [rumext.alpha :as mf] + [okulary.core :as l] [beicon.core :as rx] [app.main.streams :as ms] [app.main.ui.hooks :as hooks] @@ -21,6 +22,7 @@ [app.main.ui.shapes.image :as image] [app.main.data.workspace.selection :as dws] [app.main.store :as st] + [app.main.refs :as refs] ;; Shapes that has some peculiarities are defined in its own ;; namespace under app.ui.workspace.shapes.* prefix, all the @@ -68,18 +70,30 @@ (fn [] (st/emit! (dws/change-hover-state id false))))) +(defn make-is-moving-ref + [id] + (let [check-moving (fn [local] + (and (= :move (:transform local)) + (contains? (:selected local) id)))] + (l/derived check-moving refs/workspace-local))) + (mf/defc shape-wrapper {::mf/wrap [#(mf/memo' % shape-wrapper-memo-equals?)] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") + ghost? (unchecked-get props "ghost?") shape (geom/transform-shape frame shape) opts #js {:shape shape :frame frame} alt? (mf/use-state false) on-mouse-enter (use-mouse-enter shape) - on-mouse-leave (use-mouse-leave shape)] + on-mouse-leave (use-mouse-leave shape) + + moving-iref (mf/use-memo (mf/deps (:id shape)) + #(make-is-moving-ref (:id shape))) + moving? (mf/deref moving-iref)] (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) @@ -88,7 +102,9 @@ (fn [] (on-mouse-leave)))) - (when (and shape (not (:hidden shape))) + (when (and shape + (or ghost? (not moving?)) + (not (:hidden shape))) [:g.shape-wrapper {:on-mouse-enter on-mouse-enter :on-mouse-leave on-mouse-leave :style {:cursor (if @alt? cur/duplicate nil)}} diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 0fb0a0024c..1b3b469654 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -45,9 +45,30 @@ (recur (first ids) (rest ids)) false)))))) -(defn make-selected-ref - [id] - (l/derived #(contains? % id) refs/selected-shapes)) +(mf/defc frame-title + [{:keys [frame on-double-click on-mouse-over on-mouse-out]}] + (let [zoom (mf/deref refs/selected-zoom) + inv-zoom (/ 1 zoom) + {:keys [width x y]} frame + label-pos (gpt/point x (- y (/ 10 zoom)))] + [:text {:x 0 + :y 0 + :width width + :height 20 + :class "workspace-frame-label" + ;; Ensure that the label has always the same font + ;; size, regardless of zoom + ;; https://css-tricks.com/transforms-on-svg-elements/ + :transform (str + "scale(" inv-zoom ", " inv-zoom ") " + "translate(" (* zoom (:x label-pos)) ", " + (* zoom (:y label-pos)) + ")") + ;; User may also select the frame with single click in the label + :on-click on-double-click + :on-mouse-over on-mouse-over + :on-mouse-out on-mouse-out} + (:name frame)])) (defn frame-wrapper-factory [shape-wrapper] @@ -61,9 +82,8 @@ objects (unchecked-get props "objects") selected-iref (mf/use-memo (mf/deps (:id shape)) - #(make-selected-ref (:id shape))) + #(refs/make-selected-ref (:id shape))) selected? (mf/deref selected-iref) - zoom (mf/deref refs/selected-zoom) on-mouse-down (mf/use-callback (mf/deps shape) #(common/on-mouse-down % shape)) @@ -71,14 +91,9 @@ #(common/on-context-menu % shape)) shape (geom/transform-shape shape) - {:keys [x y width height]} shape - - inv-zoom (/ 1 zoom) children (mapv #(get objects %) (:shapes shape)) ds-modifier (get-in shape [:modifiers :displacement]) - label-pos (gpt/point x (- y (/ 10 zoom))) - on-double-click (mf/use-callback (mf/deps (:id shape)) @@ -106,24 +121,11 @@ :on-context-menu on-context-menu :on-double-click on-double-click :on-mouse-down on-mouse-down} - [:text {:x 0 - :y 0 - :width width - :height 20 - :class "workspace-frame-label" - ;; Ensure that the label has always the same font - ;; size, regardless of zoom - ;; https://css-tricks.com/transforms-on-svg-elements/ - :transform (str - "scale(" inv-zoom ", " inv-zoom ") " - "translate(" (* zoom (:x label-pos)) ", " - (* zoom (:y label-pos)) - ")") - ;; User may also select the frame with single click in the label - :on-click on-double-click - :on-mouse-over on-mouse-over - :on-mouse-out on-mouse-out} - (:name shape)] + + [:& frame-title {:frame shape + :on-context-menu on-context-menu + :on-double-click on-double-click + :on-mouse-down on-mouse-down}] [:g.frame {:filter (filters/filter-str filter-id shape)} [:& filters/filters {:filter-id filter-id :shape shape}] [:& frame-shape diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index d391181fa1..3f22cbf7f1 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -58,13 +58,15 @@ {::mf/wrap-props false} [props] (let [{:keys [id x1 y1 content group grow-type width height ] :as shape} (unchecked-get props "shape") - - selected (mf/deref refs/selected-shapes) + selected-iref (mf/use-memo (mf/deps (:id shape)) + #(refs/make-selected-ref (:id shape))) + selected? (mf/deref selected-iref) edition (mf/deref refs/selected-edition) - zoom (mf/deref refs/selected-zoom) + current-transform (mf/deref refs/current-transform) + + render-editor (mf/use-state false) + edition? (= edition id) - selected? (and (contains? selected id) - (= (count selected) 1)) embed-resources? (mf/use-ctx muc/embed-ctx) @@ -80,23 +82,30 @@ filter-id (mf/use-memo filters/get-filter-id)] + (mf/use-effect + (mf/deps shape edition selected? current-transform) + (fn [] (let [check? (and (#{:auto-width :auto-height} (:grow-type shape)) + selected? + (not edition?) + (not embed-resources?) + (nil? current-transform))] + (timers/schedule #(reset! render-editor check?))))) + [:g.shape {:on-double-click on-double-click :on-mouse-down on-mouse-down :on-context-menu on-context-menu :filter (filters/filter-str filter-id shape)} [:& filters/filters {:filter-id filter-id :shape shape}] [:* - (when (and (not edition?) (not embed-resources?)) + (when @render-editor [:g {:opacity 0 :style {:pointer-events "none"}} ;; We only render the component for its side-effect [:& text-shape-edit {:shape shape - :zoom zoom :read-only? true}]]) (if edition? - [:& text-shape-edit {:shape shape - :zoom zoom}] + [:& text-shape-edit {:shape shape}] [:& text/text-shape {:shape shape :selected? selected?}])]])) @@ -277,9 +286,9 @@ (mf/defc text-shape-edit {::mf/wrap [mf/memo]} - [{:keys [shape zoom read-only?] :or {read-only? false} :as props}] + [{:keys [shape read-only?] :or {read-only? false} :as props}] (let [{:keys [id x y width height content grow-type]} shape - + zoom (mf/deref refs/selected-zoom) state (mf/use-state #(parse-content content)) editor (mf/use-memo #(dwt/create-editor)) self-ref (mf/use-ref) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index d3870a9098..42b89610c2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -186,9 +186,9 @@ :name (:name item)})] (mf/use-effect - (mf/deps selected?) + (mf/deps selected) (fn [] - (when selected? + (when (and (= (count selected) 1) selected?) (.scrollIntoView (mf/ref-val dref) false)))) [:li {:on-context-menu on-context-menu diff --git a/frontend/src/app/main/ui/workspace/snap_distances.cljs b/frontend/src/app/main/ui/workspace/snap_distances.cljs index 9a0c4d241a..fc7f00f3c3 100644 --- a/frontend/src/app/main/ui/workspace/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/snap_distances.cljs @@ -247,12 +247,16 @@ transform (unchecked-get props "transform") selected-shapes (mf/deref (refs/objects-by-id selected)) frame-id (-> selected-shapes first :frame-id) - frame (mf/deref (refs/object-by-id frame-id))] + frame (mf/deref (refs/object-by-id frame-id)) + local (mf/deref refs/workspace-local) + + update-shape (fn [shape] (-> shape + (update :modifiers merge (:modifiers local)) + gsh/transform-shape))] (when (and (contains? layout :dynamic-alignment) (= transform :move) (not (empty? selected))) - (let [shapes (map gsh/transform-shape selected-shapes) - selrect (gsh/selection-rect shapes) + (let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect) key (->> selected (map str) (str/join "-"))] [:g.distance [:& shape-distance diff --git a/frontend/src/app/main/ui/workspace/snap_points.cljs b/frontend/src/app/main/ui/workspace/snap_points.cljs index addf0094d7..b0934c370f 100644 --- a/frontend/src/app/main/ui/workspace/snap_points.cljs +++ b/frontend/src/app/main/ui/workspace/snap_points.cljs @@ -56,11 +56,15 @@ :opacity line-opacity}]) (defn get-snap - [coord {:keys [shapes page-id filter-shapes]}] + [coord {:keys [shapes page-id filter-shapes local]}] (let [shape (if (> (count shapes) 1) (->> shapes (map gsh/transform-shape) gsh/selection-rect) (->> shapes (first))) + shape (if (:modifiers local) + (-> shape (assoc :modifiers (:modifiers local)) gsh/transform-shape) + shape) + frame-id (snap/snap-frame-id shapes)] (->> (rx/of shape) @@ -104,7 +108,7 @@ (hash-map coord fixedv (flip coord) maxv)])))) (mf/defc snap-feedback - [{:keys [shapes page-id filter-shapes zoom] :as props}] + [{:keys [shapes page-id filter-shapes zoom local] :as props}] (let [state (mf/use-state []) subject (mf/use-memo #(rx/subject)) @@ -129,7 +133,7 @@ #(rx/dispose! sub)))) (mf/use-effect - (mf/deps shapes) + (mf/deps shapes local) (fn [] (rx/push! subject props))) @@ -150,7 +154,7 @@ (mf/defc snap-points {::mf/wrap [mf/memo]} - [{:keys [layout zoom selected page-id drawing transform] :as props}] + [{:keys [layout zoom selected page-id drawing transform local] :as props}] (let [shapes (mf/deref (refs/objects-by-id selected)) filter-shapes (mf/deref refs/selected-shapes-with-children) filter-shapes (fn [id] @@ -166,5 +170,6 @@ [:& snap-feedback {:shapes shapes :page-id page-id :filter-shapes filter-shapes - :zoom zoom}]))) + :zoom zoom + :local local}]))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 6f6a5ad66b..1144686629 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -148,26 +148,35 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false} [props] - (let [data (mf/deref refs/workspace-page) - hover (unchecked-get props "hover") + (let [hover (unchecked-get props "hover") selected (unchecked-get props "selected") + ids (unchecked-get props "ids") + ghost? (unchecked-get props "ghost?") + data (mf/deref refs/workspace-page) objects (:objects data) root (get objects uuid/zero) shapes (->> (:shapes root) - (map #(get objects %)))] + (map #(get objects %))) + + shapes (if ids + (->> ids (map #(get objects %))) + shapes)] [:* [:g.shapes (for [item shapes] (if (= (:type item) :frame) [:& frame-wrapper {:shape item :key (:id item) - :objects objects}] + :objects objects + :ghost? ghost?}] [:& shape-wrapper {:shape item - :key (:id item)}]))] + :key (:id item) + :ghost? ghost?}]))] - [:& shape-outlines {:objects objects - :selected selected - :hover hover}]])) + (when (not ghost?) + [:& shape-outlines {:objects objects + :selected selected + :hover hover}])])) (defn format-viewbox [vbox] (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0)) @@ -286,6 +295,12 @@ panning picking-color?]} local + selrect-orig (->> (mf/deref refs/selected-objects) + gsh/selection-rect) + selrect (-> selrect-orig + (assoc :modifiers (:modifiers local)) + (gsh/transform-shape)) + file (mf/deref refs/workspace-file) viewport-ref (mf/use-ref nil) zoom-view-ref (mf/use-ref nil) @@ -610,6 +625,18 @@ :hover (:hover local) :selected (:selected selected)}] + (when (= :move (:transform local)) + [:svg.ghost + {:x (:x selrect) + :y (:y selrect) + :width (:width selrect) + :height (:height selrect) + :style {:pointer-events "none"}} + + [:g {:transform (str/fmt "translate(%s,%s)" (- (:x selrect-orig)) (- (:y selrect-orig)))} + [:& frames {:ids selected + :ghost? true}]]]) + (when (seq selected) [:& selection-handlers {:selected selected :zoom zoom @@ -628,7 +655,8 @@ :drawing drawing-obj :zoom zoom :page-id page-id - :selected selected}] + :selected selected + :local local}] [:& snap-distances {:layout layout :zoom zoom