diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc index f8acee0d3f..a8db384076 100644 --- a/common/src/app/common/colors.cljc +++ b/common/src/app/common/colors.cljc @@ -15,4 +15,5 @@ (def info "#59B9E2") (def test "#fabada") (def white "#FFFFFF") +(def primary "#31EFB8") diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index d2d08f1558..2ed754a816 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -30,6 +30,7 @@ [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.groups :as dwg] + [app.main.data.workspace.guides :as dwgu] [app.main.data.workspace.interactions :as dwi] [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.libraries :as dwl] @@ -2033,3 +2034,8 @@ ;; Shapes to path (d/export dwps/convert-selected-to-path) + +;; Guides +(d/export dwgu/update-guides) +(d/export dwgu/remove-guide) + diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs new file mode 100644 index 0000000000..612b4adf7e --- /dev/null +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -0,0 +1,70 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.guides + (:require + [app.main.data.workspace.changes :as dwc] + [app.main.data.workspace.state-helpers :as wsh] + [beicon.core :as rx] + [potok.core :as ptk])) + +(defn make-update-guide [guide] + (fn [other] + (cond-> other + (= (:id other) (:id guide)) + (merge guide)))) + +(defn update-guides [guide] + (ptk/reify ::update-guides + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + guides (-> state wsh/lookup-page-options (:guides [])) + guides-ids? (into #{} (map :id) guides) + + new-guides + (if (guides-ids? (:id guide)) + ;; Update existing guide + (mapv (make-update-guide guide) guides) + + ;; Add new guide + (conj guides guide)) + + rch [{:type :set-option + :page-id page-id + :option :guides + :value new-guides}] + uch [{:type :set-option + :page-id page-id + :option :guides + :value guides}]] + (rx/of + (dwc/commit-changes + {:redo-changes rch + :undo-changes uch + :origin it})))))) + +(defn remove-guide [guide] + (ptk/reify ::remove-guide + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + guides (-> state wsh/lookup-page-options (:guides [])) + new-guides (filterv #(not= (:id %) (:id guide)) guides) + + rch [{:type :set-option + :page-id page-id + :option :guides + :value new-guides}] + uch [{:type :set-option + :page-id page-id + :option :guides + :value guides}]] + (rx/of + (dwc/commit-changes + {:redo-changes rch + :undo-changes uch + :origin it})))))) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 43f89e04e6..541fb0b843 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -20,7 +20,6 @@ [app.main.ui.workspace.header :refer [header]] [app.main.ui.workspace.left-toolbar :refer [left-toolbar]] [app.main.ui.workspace.libraries] - [app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.viewport :refer [viewport]] [app.util.dom :as dom] @@ -31,45 +30,22 @@ ;; --- Workspace -(mf/defc workspace-rules - {::mf/wrap-props false - ::mf/wrap [mf/memo]} - [props] - (let [zoom (or (obj/get props "zoom") 1) - vbox (obj/get props "vbox") - vport (obj/get props "vport") - colorpalette? (obj/get props "colorpalette?")] - - [:* - [:div.empty-rule-square] - [:& horizontal-rule {:zoom zoom - :vbox vbox - :vport vport}] - [:& vertical-rule {:zoom zoom - :vbox vbox - :vport vport}] - [:& coordinates/coordinates {:colorpalette? colorpalette?}]])) - (mf/defc workspace-content {::mf/wrap-props false} [props] (let [selected (mf/deref refs/selected-shapes) local (mf/deref refs/viewport-data) - {:keys [zoom vbox vport options-mode]} local + {:keys [options-mode]} local file (obj/get props "file") - layout (obj/get props "layout")] + layout (obj/get props "layout") + colorpalette? (:colorpalette layout)] [:* - (when (:colorpalette layout) - [:& colorpalette]) + (when colorpalette? [:& colorpalette]) [:section.workspace-content [:section.workspace-viewport - (when (contains? layout :rules) - [:& workspace-rules {:zoom zoom - :vbox vbox - :vport vport - :colorpalette? (contains? layout :colorpalette)}]) + [:& coordinates/coordinates {:colorpalette? colorpalette?}] [:& viewport {:file file :local local diff --git a/frontend/src/app/main/ui/workspace/rules.cljs b/frontend/src/app/main/ui/workspace/rules.cljs deleted file mode 100644 index a9d5b34b2e..0000000000 --- a/frontend/src/app/main/ui/workspace/rules.cljs +++ /dev/null @@ -1,122 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) UXBOX Labs SL - -(ns app.main.ui.workspace.rules - (:require - [app.common.colors :as colors] - [app.common.math :as mth] - [app.util.object :as obj] - [rumext.alpha :as mf])) - -(defn- calculate-step-size - [zoom] - (cond - (< 0 zoom 0.008) 10000 - (< 0.008 zoom 0.015) 5000 - (< 0.015 zoom 0.04) 2500 - (< 0.04 zoom 0.07) 1000 - (< 0.07 zoom 0.2) 500 - (< 0.2 zoom 0.5) 250 - (< 0.5 zoom 1) 100 - (<= 1 zoom 2) 50 - (< 2 zoom 4) 25 - (< 4 zoom 6) 10 - (< 6 zoom 15) 5 - (< 15 zoom 25) 2 - (< 25 zoom) 1 - :else 1)) - -(defn draw-rule! - [dctx {:keys [zoom size start type]}] - (when start - (let [txfm (- (* (- 0 start) zoom) 20) - step (calculate-step-size zoom) - - minv (max (mth/round start) -100000) - minv (* (mth/ceil (/ minv step)) step) - - maxv (min (mth/round (+ start (/ size zoom))) 100000) - maxv (* (mth/floor (/ maxv step)) step) - - path (js/Path2D.)] - - (if (= type :horizontal) - (.translate dctx txfm 0) - (.translate dctx 0 txfm)) - - (obj/set! dctx "font" "12px worksans") - (obj/set! dctx "fillStyle" colors/gray-30) - (obj/set! dctx "strokeStyle" colors/gray-30) - (obj/set! dctx "textAlign" "center") - - (loop [i minv] - (if (<= i maxv) - (let [pos (+ (* i zoom) 0)] - (.save dctx) - (if (= type :horizontal) - (do - ;; Write the rule numbers - (.fillText dctx (str i) pos 13) - - ;; Build the rules lines - (.moveTo path pos 17) - (.lineTo path pos 20)) - (do - ;; Write the rule numbers - (.translate dctx 12 pos) - (.rotate dctx (/ (* 270 js/Math.PI) 180)) - (.fillText dctx (str i) 0 0) - - ;; Build the rules lines - (.moveTo path 17 pos) - (.lineTo path 20 pos))) - (.restore dctx) - (recur (+ i step))) - - ;; Put the path in the canvas - (.stroke dctx path)))))) - - -(mf/defc horizontal-rule - [{:keys [zoom vbox vport] :as props}] - (let [canvas (mf/use-ref) - width (- (:width vport) 20)] - (mf/use-layout-effect - (mf/deps zoom width (:x vbox)) - (fn [] - (let [node (mf/ref-val canvas) - dctx (.getContext ^js node "2d")] - (obj/set! node "width" width) - (draw-rule! dctx {:zoom zoom - :type :horizontal - :size width - :start (+ (:x vbox) (:left-offset vbox))})))) - - [:canvas.horizontal-rule - {:ref canvas - :width width - :height 20}])) - -(mf/defc vertical-rule - [{:keys [zoom vbox vport] :as props}] - (let [canvas (mf/use-ref) - height (- (:height vport) 20)] - (mf/use-layout-effect - (mf/deps zoom height (:y vbox)) - (fn [] - (let [node (mf/ref-val canvas) - dctx (.getContext ^js node "2d")] - (obj/set! node "height" height) - (draw-rule! dctx {:zoom zoom - :type :vertical - :size height - :count 100 - :start (:y vbox)})))) - - [:canvas.vertical-rule - {:ref canvas - :width 20 - :height height}])) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index bb1eb36056..015d39e3e5 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -21,11 +21,13 @@ [app.main.ui.workspace.viewport.drawarea :as drawarea] [app.main.ui.workspace.viewport.frame-grid :as frame-grid] [app.main.ui.workspace.viewport.gradients :as gradients] + [app.main.ui.workspace.viewport.guides :as guides] [app.main.ui.workspace.viewport.hooks :as hooks] [app.main.ui.workspace.viewport.interactions :as interactions] [app.main.ui.workspace.viewport.outline :as outline] [app.main.ui.workspace.viewport.pixel-overlay :as pixel-overlay] [app.main.ui.workspace.viewport.presence :as presence] + [app.main.ui.workspace.viewport.rules :as rules] [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] @@ -89,6 +91,13 @@ ;; STREAMS move-stream (mf/use-memo #(rx/subject)) + frame-parent (mf/use-memo + (mf/deps @hover-ids base-objects) + (fn [] + (let [parent (get base-objects (last @hover-ids))] + (when (= :frame (:type parent)) + parent)))) + zoom (d/check-num zoom 1) drawing-tool (:tool drawing) drawing-obj (:object drawing) @@ -145,7 +154,11 @@ (or drawing-obj transform)) show-selrect? (and selrect (empty? drawing)) 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? (contains? layout :rules) + + ;; TODO + show-guides? true] (hooks/setup-dom-events viewport-ref zoom disable-paste in-viewport?) (hooks/setup-viewport-size viewport-ref) @@ -157,6 +170,8 @@ (hooks/setup-shortcuts node-editing? drawing-path?) (hooks/setup-active-frames base-objects vbox hover active-frames) + + [:div.viewport [:div.viewport-overlays @@ -294,7 +309,9 @@ (when show-grids? [:& frame-grid/frame-grid - {:zoom zoom :selected selected :transform transform}]) + {:zoom zoom + :selected selected + :transform transform}]) (when show-pixel-grid? [:& widgets/pixel-grid @@ -325,12 +342,6 @@ {:zoom zoom :tooltip tooltip}]) - (when show-presence? - [:& presence/active-cursors - {:page-id page-id}]) - - [:& widgets/viewport-actions] - (when show-prototypes? [:& interactions/interactions {:selected selected @@ -341,5 +352,22 @@ (when show-selrect? [:& widgets/selection-rect {:data selrect - :zoom zoom}])]]])) + :zoom zoom}]) + + (when show-presence? + [:& presence/active-cursors + {:page-id page-id}]) + + [:& widgets/viewport-actions] + + (when show-rules? + [:& rules/rules + {:zoom zoom + :vbox vbox}]) + + (when show-guides? + [:& guides/viewport-guides + {:zoom zoom + :vbox vbox + :hover-frame frame-parent}])]]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs new file mode 100644 index 0000000000..64e846e338 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -0,0 +1,437 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.viewport.guides + (:require + [app.common.colors :as colors] + [app.common.math :as mth] + [app.common.uuid :as uuid] + [app.main.data.workspace :as dw] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.streams :as ms] + [app.main.ui.workspace.viewport.rules :as rules] + [app.util.dom :as dom] + [rumext.alpha :as mf])) + +(def guide-width 1) +(def guide-opacity 0.7) +(def guide-opacity-hover 1) +(def guide-color colors/primary) +(def guide-pill-width 34) +(def guide-pill-height 20) +(def guide-pill-corner-radius 4) +(def guide-active-area 16) + +(defn use-guide + "Hooks to support drag/drop for existing guides and new guides" + [on-guide-change get-hover-frame zoom {:keys [position axis frame-id]}] + (let [dragging-ref (mf/use-ref false) + start-ref (mf/use-ref nil) + start-pos-ref (mf/use-ref nil) + state (mf/use-state {:hover false + :new-position nil + :new-frame-id frame-id}) + + frame-id (:new-frame-id @state) + + frame-ref (mf/use-memo (mf/deps frame-id) #(refs/object-by-id frame-id)) + frame (mf/deref frame-ref) + + on-pointer-enter + (mf/use-callback + (fn [] + (swap! state assoc :hover true))) + + on-pointer-leave + (mf/use-callback + (fn [] + (swap! state assoc :hover false))) + + on-pointer-down + (mf/use-callback + (fn [event] + (dom/capture-pointer event) + (mf/set-ref-val! dragging-ref true) + (mf/set-ref-val! start-ref (dom/get-client-position event)) + (mf/set-ref-val! start-pos-ref (get @ms/mouse-position axis)))) + + on-pointer-up + (mf/use-callback + (mf/deps (select-keys @state [:new-position :new-frame-id]) on-guide-change) + (fn [] + (when (some? on-guide-change) + (when (some? (:new-position @state)) + (on-guide-change {:position (:new-position @state) + :frame-id (:new-frame-id @state)}))))) + + on-lost-pointer-capture + (mf/use-callback + (fn [event] + (dom/release-pointer event) + (mf/set-ref-val! dragging-ref false) + (mf/set-ref-val! start-ref nil) + (mf/set-ref-val! start-pos-ref nil) + (swap! state assoc :new-position nil))) + + on-mouse-move + (mf/use-callback + (mf/deps position zoom) + (fn [event] + + (when-let [_ (mf/ref-val dragging-ref)] + (let [start-pt (mf/ref-val start-ref) + start-pos (mf/ref-val start-pos-ref) + current-pt (dom/get-client-position event) + delta (/ (- (get current-pt axis) (get start-pt axis)) zoom) + new-position (if (some? position) + (+ position delta) + (+ start-pos delta)) + + ;; TODO: Change when pixel-grid flag exists + new-position (mth/round new-position) + new-frame-id (:id (get-hover-frame))] + #_(prn ">>" new-position new-frame-id) + (swap! state assoc + :new-position new-position + :new-frame-id new-frame-id)))))] + {:on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up + :on-lost-pointer-capture on-lost-pointer-capture + :on-mouse-move on-mouse-move + :state state + :frame frame})) + +;; This functions are auxiliary to get the coords of components depending on the axis +;; we're handling + +(defn guide-area-axis + [pos vbox zoom frame axis] + (let [rules-pos (/ rules/rules-pos zoom) + guide-active-area (/ guide-active-area zoom)] + (cond + (and (some? frame) (= axis :x)) + {:x (- pos (/ guide-active-area 2)) + :y (:y frame) + :width guide-active-area + :height (:height frame)} + + (some? frame) + {:x (:x frame) + :y (- pos (/ guide-active-area 2)) + :width (:width frame) + :height guide-active-area} + + (= axis :x) + {:x (- pos (/ guide-active-area 2)) + :y (+ (:y vbox) rules-pos) + :width guide-active-area + :height (:height vbox)} + + :else + {:x (+ (:x vbox) rules-pos) + :y (- pos (/ guide-active-area 2)) + :width (:width vbox) + :height guide-active-area} + + + + ))) + +(defn guide-line-axis + ([pos vbox axis] + (if (= axis :x) + {:x1 pos + :y1 (:y vbox) + :x2 pos + :y2 (+ (:y vbox) (:height vbox))} + + {:x1 (:x vbox) + :y1 pos + :x2 (+ (:x vbox) (:width vbox)) + :y2 pos})) + + ([pos vbox frame axis] + (if (= axis :x) + {:l1-x1 pos + :l1-y1 (:y vbox) + :l1-x2 pos + :l1-y2 (:y frame) + :l2-x1 pos + :l2-y1 (:y frame) + :l2-x2 pos + :l2-y2 (+ (:y frame) (:height frame)) + :l3-x1 pos + :l3-y1 (+ (:y frame) (:height frame)) + :l3-x2 pos + :l3-y2 (+ (:y vbox) (:height vbox))} + {:l1-x1 (:x vbox) + :l1-y1 pos + :l1-x2 (:x frame) + :l1-y2 pos + :l2-x1 (:x frame) + :l2-y1 pos + :l2-x2 (+ (:x frame) (:width frame)) + :l2-y2 pos + :l3-x1 (+ (:x frame) (:width frame)) + :l3-y1 pos + :l3-x2 (+ (:x vbox) (:width vbox)) + :l3-y2 pos}))) + +(defn guide-pill-axis + [pos vbox zoom axis] + (let [rules-pos (/ rules/rules-pos zoom) + guide-pill-width (/ guide-pill-width zoom) + guide-pill-height (/ guide-pill-height zoom)] + + (if (= axis :x) + {:rect-x (- pos (/ guide-pill-width 2)) + :rect-y (+ (:y vbox) rules-pos (- (/ guide-pill-width 2)) (/ 2 zoom)) + :rect-width guide-pill-width + :rect-height guide-pill-height + :text-x pos + :text-y (+ (:y vbox) rules-pos (- (/ 3 zoom)))} + + {:rect-x (+ (:x vbox) rules-pos (- (/ guide-pill-height 2)) (- (/ 5 zoom))) + :rect-y (- pos (/ guide-pill-width 2)) + :rect-width guide-pill-height + :rect-height guide-pill-width + :text-x (+ (:x vbox) rules-pos (- (/ 3 zoom))) + :text-y pos}))) + +(defn guide-inside-vbox? + ([vbox] + (partial guide-inside-vbox? vbox)) + + ([{:keys [x y width height]} {:keys [axis position]}] + (let [x1 x + x2 (+ x width) + y1 y + y2 (+ y height)] + (if (= axis :x) + (and (>= position x1) + (<= position x2)) + (and (>= position y1) + (<= position y2)))))) + +(defn guide-creation-area + [vbox zoom axis] + (if (= axis :x) + {:x (:x vbox) + :y (:y vbox) + :width (/ 24 zoom) + :height (:height vbox)} + + {:x (:x vbox) + :y (:y vbox) + :width (:width vbox) + :height (/ 24 zoom)})) + +(mf/defc guide + {::mf/wrap [mf/memo]} + [{:keys [guide hover? on-guide-change get-hover-frame vbox zoom hover-frame]}] + + (let [axis (:axis guide) + + handle-change-position + (mf/use-callback + (mf/deps on-guide-change) + (fn [changes] + (when on-guide-change + (on-guide-change (merge guide changes))))) + + {:keys [on-pointer-enter + on-pointer-leave + on-pointer-down + on-pointer-up + on-lost-pointer-capture + on-mouse-move + state + frame]} (use-guide handle-change-position get-hover-frame zoom guide) + + frame (or frame hover-frame) + pos (or (:new-position @state) (:position guide)) + guide-width (/ guide-width zoom) + guide-pill-corner-radius (/ guide-pill-corner-radius zoom)] + + [:g.guide-area + (let [{:keys [x y width height]} (guide-area-axis pos vbox zoom frame axis)] + [:rect {:x x + :y y + :width width + :height height + :style {:fill "none" + :pointer-events "fill" + :cursor (if (= axis :x) "ew-resize" "ns-resize")} + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up + :on-lost-pointer-capture on-lost-pointer-capture + :on-mouse-move on-mouse-move}]) + + (if (some? frame) + (let [{:keys [l1-x1 l1-y1 l1-x2 l1-y2 + l2-x1 l2-y1 l2-x2 l2-y2 + l3-x1 l3-y1 l3-x2 l3-y2]} + (guide-line-axis pos vbox frame axis)] + [:g + (when (or hover? (:hover @state)) + [:line {:x1 l1-x1 + :y1 l1-y1 + :x2 l1-x2 + :y2 l1-y2 + :style {:stroke guide-color + :stroke-opacity guide-opacity-hover + :stroke-dasharray (str "0, " (/ 6 zoom)) + :stroke-linecap "round" + :stroke-width guide-width}}]) + [:line {:x1 l2-x1 + :y1 l2-y1 + :x2 l2-x2 + :y2 l2-y2 + :style {:stroke guide-color + :stroke-width guide-width + :stroke-opacity (if (or hover? (:hover @state)) + guide-opacity-hover + guide-opacity)}}] + (when (or hover? (:hover @state)) + [:line {:x1 l3-x1 + :y1 l3-y1 + :x2 l3-x2 + :y2 l3-y2 + :style {:stroke guide-color + :stroke-opacity guide-opacity-hover + :stroke-width guide-width + :stroke-dasharray (str "0, " (/ 6 zoom)) + :stroke-linecap "round"}}])]) + + (let [{:keys [x1 y1 x2 y2]} (guide-line-axis pos vbox axis)] + [:line {:x1 x1 + :y1 y1 + :x2 x2 + :y2 y2 + :style {:stroke guide-color + :stroke-width guide-width + :stroke-opacity (if (or hover? (:hover @state)) + guide-opacity-hover + guide-opacity)}}])) + + (when (or hover? (:hover @state)) + (let [{:keys [rect-x rect-y rect-width rect-height text-x text-y]} + (guide-pill-axis pos vbox zoom axis)] + [:g.guide-pill + [:rect {:x rect-x + :y rect-y + :width rect-width + :height rect-height + :rx guide-pill-corner-radius + :ry guide-pill-corner-radius + :style {:fill guide-color}}] + + [:text {:x text-x + :y text-y + :text-anchor "middle" + :dominant-baseline "middle" + :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) + :style {:font-size (/ 13 zoom) + :font-family "sourcesanspro" + :fill colors/black}} + (str (mth/round pos))]]))])) + +(mf/defc new-guide-area + [{:keys [vbox zoom axis get-hover-frame]}] + + (let [on-guide-change + (mf/use-callback + (mf/deps vbox) + (fn [guide] + (let [guide (-> guide + (assoc :id (uuid/next) + :axis axis))] + (when (guide-inside-vbox? vbox guide) + (st/emit! (dw/update-guides guide)))))) + + {:keys [on-pointer-enter + on-pointer-leave + on-pointer-down + on-pointer-up + on-lost-pointer-capture + on-mouse-move + state + frame]} (use-guide on-guide-change get-hover-frame zoom {:axis axis})] + + [:g.new-guides + (let [{:keys [x y width height]} (guide-creation-area vbox zoom axis)] + [:rect {:x x + :y y + :width width + :height height + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-down on-pointer-down + :on-pointer-up on-pointer-up + :on-lost-pointer-capture on-lost-pointer-capture + :on-mouse-move on-mouse-move + :style {:fill "none" + :pointer-events "fill" + :cursor (if (= axis :x) "ew-resize" "ns-resize")}}]) + + (when (:new-position @state) + [:& guide {:guide {:axis axis + :position (:new-position @state)} + :get-hover-frame get-hover-frame + :vbox vbox + :zoom zoom + :hover? true + :hover-frame frame}])])) + +(mf/defc viewport-guides + {::mf/wrap [mf/memo]} + [{:keys [zoom vbox hover-frame]}] + + (let [page (mf/deref refs/workspace-page) + guides (->> (get-in page [:options :guides] []) + (filter (guide-inside-vbox? vbox))) + + hover-frame-ref (mf/use-ref nil) + + ;; We use the ref to not redraw every guide everytime the hovering frame change + ;; we're only interested to get the frame in the guide we're moving + get-hover-frame + (mf/use-callback + (fn [] + (mf/ref-val hover-frame-ref))) + + on-guide-change + (mf/use-callback + (mf/deps vbox) + (fn [guide] + (if (guide-inside-vbox? vbox guide) + (st/emit! (dw/update-guides guide)) + (st/emit! (dw/remove-guide guide)))))] + + #_(mf/use-effect (mf/deps guides) #(.log js/console (clj->js guides))) + (mf/use-effect + (mf/deps hover-frame) + (fn [] + #_(.log js/console "set" (clj->js hover-frame)) + (mf/set-ref-val! hover-frame-ref hover-frame))) + + [:g.guides {:pointer-events "none"} + [:& new-guide-area {:vbox vbox :zoom zoom :axis :x :get-hover-frame get-hover-frame}] + [:& new-guide-area {:vbox vbox :zoom zoom :axis :y :get-hover-frame get-hover-frame}] + + (for [current guides] + [:& guide {:key (str "guide-" (:id current)) + :guide current + :vbox vbox + :zoom zoom + :get-hover-frame get-hover-frame + :on-guide-change on-guide-change}])])) + diff --git a/frontend/src/app/main/ui/workspace/viewport/rules.cljs b/frontend/src/app/main/ui/workspace/viewport/rules.cljs new file mode 100644 index 0000000000..6b21452744 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/rules.cljs @@ -0,0 +1,137 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.workspace.viewport.rules + (:require + [app.common.colors :as colors] + [app.common.data :as d] + [app.common.math :as mth] + [app.util.object :as obj] + [rumext.alpha :as mf])) + +(def rules-pos 15) +(def rules-size 4) +(def rules-width 1) + +;; ---------------- +;; RULES +;; ---------------- + +(defn- calculate-step-size + [zoom] + (cond + (< 0 zoom 0.008) 10000 + (< 0.008 zoom 0.015) 5000 + (< 0.015 zoom 0.04) 2500 + (< 0.04 zoom 0.07) 1000 + (< 0.07 zoom 0.2) 500 + (< 0.2 zoom 0.5) 250 + (< 0.5 zoom 1) 100 + (<= 1 zoom 2) 50 + (< 2 zoom 4) 25 + (< 4 zoom 6) 10 + (< 6 zoom 15) 5 + (< 15 zoom 25) 2 + (< 25 zoom) 1 + :else 1)) + +(defn get-clip-area + [vbox zoom axis] + (if (= axis :x) + (let [x (+ (:x vbox) (/ 25 zoom)) + y (:y vbox) + width (- (:width vbox) (/ 21 zoom)) + height (/ 25 zoom)] + {:x x :y y :width width :height height}) + + (let [x (:x vbox) + y (+ (:y vbox) (/ 25 zoom)) + width (/ 25 zoom) + height (- (:height vbox) (/ 21 zoom))] + {:x x :y y :width width :height height}))) + +(defn get-rule-params + [vbox axis] + (if (= axis :x) + (let [start (:x vbox) + end (+ start (:width vbox))] + {:start start :end end}) + + (let [start (:y vbox) + end (+ start (:height vbox))] + {:start start :end end}))) + +(defn get-rule-axis + [val vbox zoom axis] + (let [rules-pos (/ rules-pos zoom) + rules-size (/ rules-size zoom)] + (if (= axis :x) + {:text-x val + :text-y (+ (:y vbox) (- rules-pos (/ 4 zoom))) + :line-x1 val + :line-y1 (+ (:y vbox) rules-pos (/ 2 zoom)) + :line-x2 val + :line-y2 (+ (:y vbox) rules-pos (/ 2 zoom) rules-size)} + + {:text-x (+ (:x vbox) (- rules-pos (/ 4 zoom))) + :text-y val + :line-x1 (+ (:x vbox) rules-pos (/ 2 zoom)) + :line-y1 val + :line-x2 (+ (:x vbox) rules-pos (/ 2 zoom) rules-size) + :line-y2 val}))) + +(mf/defc rules-axis + [{:keys [zoom vbox axis]}] + (let [rules-width (/ rules-width zoom) + step (calculate-step-size zoom) + clip-id (str "clip-rule-" (d/name axis))] + + [:g.rules {:clipPath (str "url(#" clip-id ")")} + + [:defs + [:clipPath {:id clip-id} + (let [{:keys [x y width height]} (get-clip-area vbox zoom axis)] + [:rect {:x x :y y :width width :height height}])]] + + (let [{:keys [start end]} (get-rule-params vbox axis) + minv (max (mth/round start) -100000) + minv (* (mth/ceil (/ minv step)) step) + maxv (min (mth/round end) 100000) + maxv (* (mth/floor (/ maxv step)) step)] + + (for [step-val (range minv (inc maxv) step)] + (let [{:keys [text-x text-y line-x1 line-y1 line-x2 line-y2]} + (get-rule-axis step-val vbox zoom axis)] + [:* + [:text {:key (str "text-" (d/name axis) "-" step-val) + :x text-x + :y text-y + :text-anchor "middle" + :dominant-baseline "middle" + :transform (when (= axis :y) (str "rotate(-90 " text-x "," text-y ")")) + :style {:font-size (/ 13 zoom) + :font-family "sourcesanspro" + :fill colors/gray-30}} + (str (mth/round step-val))] + + [:line {:key (str "line-" (d/name axis) "-" step-val) + :x1 line-x1 + :y1 line-y1 + :x2 line-x2 + :y2 line-y2 + :style {:stroke colors/gray-30 + :stroke-width rules-width}}]])))])) + +(mf/defc rules + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [props] + (let [zoom (obj/get props "zoom") + vbox (obj/get props "vbox")] + (when (some? vbox) + [:g.rules {:pointer-events "none"} + [:& rules-axis {:zoom zoom :vbox vbox :axis :x}] + [:& rules-axis {:zoom zoom :vbox vbox :axis :y}]])))