diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss index 81e163680..0d59b6392 100644 --- a/frontend/resources/styles/main/partials/workspace.scss +++ b/frontend/resources/styles/main/partials/workspace.scss @@ -120,12 +120,18 @@ .workspace-viewport { height: 100%; - overflow: scroll; + overflow: hidden; transition: none; width: 100%; - margin-top: 20px; + + display: grid; + grid-template-rows: 20px 1fr; + grid-template-columns: 20px 1fr; .viewport { + grid-column: 2 / span 1; + grid-row: 2 / span 1; + &.drawing { cursor: cell; } @@ -157,11 +163,21 @@ /* Rules */ + .empty-rule-square { + grid-column: 1 / span 1; + grid-row: 1 / span 1; + background-color: $color-canvas; + z-index: 13; + border-right: 1px solid $color-gray-30; + border-bottom: 1px solid $color-gray-30; + } + .horizontal-rule { transition: none; - position: absolute; pointer-events: none; - top: 0px; + grid-column: 2 / span 1; + grid-row: 1 / span 1; + z-index: 13; rect { fill: $color-canvas; @@ -174,7 +190,9 @@ .vertical-rule { transition: none; pointer-events: none; - left: 0px; + grid-column: 1 / span 1; + grid-row: 2 / span 1; + z-index: 13; rect { fill: $color-canvas; diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 80dab0176..9efdfa641 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -72,6 +72,12 @@ (def workspace-default {:zoom 1 + :size {:x 0 + :y 0 + :width c/viewport-width + :height c/viewport-width + :viewport-width c/viewport-width + :viewport-height c/viewport-height} :flags #{} :selected #{} :drawing nil @@ -189,6 +195,56 @@ ;; Workspace State Manipulation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- Viewport Sizing + +(defn initialize-viewport + [{:keys [width height] :as size}] + (ptk/reify ::initialize-viewport + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + (fn [local] + (-> local + (assoc :zoom 1) + (update :size assoc + :x 0 + :y 0 + :width width + :height height + :viewport-width width + :viewport-height height))))))) + + + +(defn update-viewport-position + [{:keys [x y] :or {x identity y identity}}] + (ptk/reify ::update-viewport-position + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :size] + (fn [size] + (-> size + (update :x x) + (update :y y))))))) + +(defn update-viewport-size + [{:keys [width height]}] + (ptk/reify ::update-viewport-size + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local + (fn [{:keys [size] :as local}] + (let [wprop (/ (:viewport-width size) width) + hprop (/ (:viewport-height size) height) + size' (-> size + (assoc :viewport-width width) + (assoc :viewport-height height) + (update :width #(/ % wprop)) + (update :height #(/ % hprop)))] + (assoc local :size size'))))))) + +;; --- + (defn adjust-group-shapes [ids] (ptk/reify ::adjust-group-shapes @@ -269,41 +325,103 @@ ;; --- Zoom Management -(def increase-zoom +(defn- impl-update-zoom + [local zoom] + (let [base-width (get-in local [:size :viewport-width]) + base-height (get-in local [:size :viewport-height]) + + zoom (if (fn? zoom) + (zoom (:zoom local)) + zoom) + + width (/ base-width zoom) + height (/ base-height zoom)] + (-> local + (assoc :zoom zoom) + (update :size assoc :width width :height height)))) + +(defn increase-zoom + [center] (ptk/reify ::increase-zoom ptk/UpdateEvent (update [_ state] - (let [increase #(nth c/zoom-levels - (+ (d/index-of c/zoom-levels %) 1) - (last c/zoom-levels))] - (update-in state [:workspace-local :zoom] (fnil increase 1)))))) + (update state :workspace-local + #(impl-update-zoom % (fn [z] (* z 1.1))))))) -(def decrease-zoom +(defn decrease-zoom + [center] (ptk/reify ::decrease-zoom ptk/UpdateEvent (update [_ state] - (let [decrease #(nth c/zoom-levels - (- (d/index-of c/zoom-levels %) 1) - (first c/zoom-levels))] - (update-in state [:workspace-local :zoom] (fnil decrease 1)))))) + (update state :workspace-local + #(impl-update-zoom % (fn [z] (* z 0.9))))))) (def reset-zoom (ptk/reify ::reset-zoom ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :zoom] 1)))) + (update state :workspace-local + #(impl-update-zoom % 1))))) (def zoom-to-50 (ptk/reify ::zoom-to-50 ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :zoom] 0.5)))) + (update state :workspace-local + #(impl-update-zoom % 0.5))))) (def zoom-to-200 (ptk/reify ::zoom-to-200 ptk/UpdateEvent (update [_ state] - (assoc-in state [:workspace-local :zoom] 2)))) + (update state :workspace-local + #(impl-update-zoom % 2))))) + +;; (defn impl-expand-rect +;; [{:keys [x y width height] :as rect} padding] +;; (assoc rect +;; :x (- x padding) +;; :y (- y padding) +;; :width (+ width padding) +;; :height (+ height padding))) + +;; (def zoom-to-selected-shape +;; (ptk/reify ::zoom-to-selected-shape +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [page-id (get-in state [:workspace-page :id]) +;; objects (get-in state [:workspace-data page-id :objects]) +;; selected (get-in state [:workspace-local :selected]) +;; shapes (map #(get objects %) selected) +;; rect (geom/selection-rect shapes) +;; rect (impl-expand-rect rect 20) +;; ] +;; (update state :workspace-local +;; (fn [{:keys [size] :as local}] +;; (let [proportion (/ (:viewport-width size) (:viewport-height size)) +;; width (:width rect) +;; height (/ (:width rect) proportion) + +;; [width height] (if (> (:height rect) height) +;; [(* width proportion) (:height rect)] +;; [width height]) + +;; zoom (/ (:viewport-width size) width)] + +;; ;; (js/console.log "proportion=" proportion +;; ;; "width=" width +;; ;; "height=" height +;; ;; "viewport-width=" (:viewport-width size) +;; ;; "viewport-height=" (:viewport-height size)) +;; (-> local +;; (assoc :zoom zoom) +;; (update :size assoc +;; :x (:x rect) +;; :y (:y rect) +;; :width width +;; :height height +;; ))))))))) + ;; --- Selection Rect @@ -328,8 +446,8 @@ {:type :rect :x start-x :y start-y - :width (- end-x start-x) - :height (- end-y start-y)}))] + :width (mth/abs (- end-x start-x)) + :height (mth/abs (- end-y start-y))}))] (ptk/reify ::handle-selection ptk/WatchEvent (watch [_ state stream] @@ -1341,6 +1459,7 @@ {"ctrl+shift+m" #(st/emit! (toggle-layout-flag :sitemap)) "ctrl+shift+i" #(st/emit! (toggle-layout-flag :libraries)) "ctrl+shift+l" #(st/emit! (toggle-layout-flag :layers)) + "ctrl+shift+r" #(st/emit! (toggle-layout-flag :rules)) "+" #(st/emit! increase-zoom) "-" #(st/emit! decrease-zoom) "ctrl+g" #(st/emit! create-group) @@ -1348,6 +1467,7 @@ "shift+0" #(st/emit! zoom-to-50) "shift+1" #(st/emit! reset-zoom) "shift+2" #(st/emit! zoom-to-200) + ;; "shift+2" #(st/emit! zoom-to-selected-shape) "ctrl+d" #(st/emit! duplicate-selected) "ctrl+z" #(st/emit! dwc/undo) "ctrl+shift+z" #(st/emit! dwc/redo) diff --git a/frontend/src/uxbox/main/data/workspace/transforms.cljs b/frontend/src/uxbox/main/data/workspace/transforms.cljs index 70300e880..e7cd4aab6 100644 --- a/frontend/src/uxbox/main/data/workspace/transforms.cljs +++ b/frontend/src/uxbox/main/data/workspace/transforms.cljs @@ -25,10 +25,6 @@ ;; -- Helpers -(defn- apply-zoom - [point] - (gpt/divide point (gpt/point @refs/selected-zoom))) - ;; For each of the 8 handlers gives the modifier for resize ;; for example, right will only grow in the x coordinate and left ;; will grow in the inverse of the x coordinate @@ -146,7 +142,6 @@ resizing-shapes (map #(get-in state [:workspace-data page-id :objects %]) ids)] (rx/concat (->> ms/mouse-position - (rx/map apply-zoom) ;; (rx/mapcat apply-grid-alignment) (rx/with-latest vector ms/mouse-position-ctrl) (rx/map normalize-proportion-lock) @@ -171,7 +166,7 @@ (let [stoper (rx/filter ms/mouse-up? stream) group (gsh/selection-rect shapes) group-center (gsh/center group) - initial-angle (gpt/angle (apply-zoom @ms/mouse-position) group-center) + initial-angle (gpt/angle @ms/mouse-position group-center) calculate-angle (fn [pos ctrl?] (let [angle (- (gpt/angle pos group-center) initial-angle) angle (if (neg? angle) (+ 360 angle) angle) @@ -187,7 +182,6 @@ angle))] (rx/concat (->> ms/mouse-position - (rx/map apply-zoom) (rx/with-latest vector ms/mouse-position-ctrl) (rx/map (fn [[pos ctrl?]] (let [delta-angle (calculate-angle pos ctrl?)] @@ -205,12 +199,11 @@ (ptk/reify ::start-move-selected ptk/WatchEvent (watch [_ state stream] - (let [initial (apply-zoom @ms/mouse-position) + (let [initial @ms/mouse-position selected (get-in state [:workspace-local :selected]) stopper (rx/filter ms/mouse-up? stream)] (->> ms/mouse-position (rx/take-until stopper) - (rx/map apply-zoom) (rx/map #(gpt/to-vec initial %)) (rx/map #(gpt/length %)) (rx/filter #(> % 0.5)) @@ -234,7 +227,6 @@ (rx/concat (->> ms/mouse-position (rx/take-until stopper) - (rx/map apply-zoom) (rx/map #(gpt/to-vec from-position %)) (rx/map (snap/closest-snap-move snap-data shapes)) (rx/map gmt/translate-matrix) diff --git a/frontend/src/uxbox/main/streams.cljs b/frontend/src/uxbox/main/streams.cljs index a8bb6d41c..31ce917d8 100644 --- a/frontend/src/uxbox/main/streams.cljs +++ b/frontend/src/uxbox/main/streams.cljs @@ -77,15 +77,20 @@ [current] (->> (rx/concat (rx/of current) (rx/sample 10 mouse-position)) - (rx/map #(gpt/divide % (gpt/point @refs/selected-zoom))) - ;; (rx/mapcat (fn [point] - ;; (if @refs/selected-alignment - ;; (uwrk/align-point point) - ;; (rx/of point)))) (rx/buffer 2 1) (rx/map (fn [[old new]] (gpt/subtract new old))))) + +(defonce mouse-position-delta + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter pointer-event?) + (rx/filter #(= :delta (:source %))) + (rx/map :pt))] + (rx/subscribe-with ob sub) + sub)) + (defonce viewport-scroll (let [sub (rx/behavior-subject nil) sob (->> (rx/filter scroll-event? st/stream) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 86dab92ad..2bbc1a035 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -45,23 +45,29 @@ right-sidebar? (not (empty? (keep layout [:icons :drawtools :element-options]))) classes (classnames :no-tool-bar-right (not right-sidebar?) - :no-tool-bar-left (not left-sidebar?))] + :no-tool-bar-left (not left-sidebar?)) + + local (mf/deref refs/workspace-local)] + [:* (when (:colorpalette layout) [:& colorpalette {:left-sidebar? left-sidebar?}]) [:section.workspace-content {:class classes} - [:& history-dialog] - ;; Rules - (when (contains? layout :rules) - [:* - [:& horizontal-rule] - [:& vertical-rule]]) + [:section.workspace-viewport + (when (contains? layout :rules) + [:* + [:div.empty-rule-square] + [:& horizontal-rule {:zoom (:zoom local) + :size (:size local)}] + [:& vertical-rule {:zoom (:zoom local 1) + :size (:size local)}]]) - [:section.workspace-viewport {:id "workspace-viewport"} - [:& viewport {:page page :file file}]]] + [:& viewport {:page page + :file file + :local local}]]] [:& left-toolbar {:page page :layout layout}] diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index d5d088019..ec74c2763 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -132,7 +132,10 @@ point-snap (snap/closest-snap-point snap-data [shape] point) deltav (gpt/to-vec initial point-snap) scalev (gpt/divide (gpt/add shapev deltav) shapev) - scalev (if lock? (let [v (max (:x scalev) (:y scalev))] (gpt/point v v)) scalev)] + scalev (if lock? + (let [v (max (:x scalev) (:y scalev))] + (gpt/point v v)) + scalev)] (-> shape (assoc-in [:modifiers :resize-vector] scalev) (assoc-in [:modifiers :resize-origin] (gpt/point x y)) @@ -144,13 +147,12 @@ (ptk/reify ::handle-drawing-generic ptk/WatchEvent (watch [_ state stream] - (let [{:keys [zoom flags]} (:workspace-local state) + (let [{:keys [flags]} (:workspace-local state) stoper? #(or (ms/mouse-up? %) (= % :interrupt)) stoper (rx/filter stoper? stream) initial @ms/mouse-position snap-data (get state :workspace-snap-data) - mouse (->> ms/mouse-position - (rx/map #(gpt/divide % (gpt/point zoom)))) + mouse ms/mouse-position page-id (get state :current-page-id) objects (get-in state [:workspace-data page-id :objects]) @@ -204,15 +206,14 @@ (ptk/reify ::handle-drawing-path ptk/WatchEvent (watch [_ state stream] - (let [{:keys [zoom flags]} (:workspace-local state) + (let [{:keys [flags]} (:workspace-local state) - last-point (volatile! (gpt/divide @ms/mouse-position (gpt/point zoom))) + last-point (volatile! @ms/mouse-position) stoper (->> (rx/filter stoper-event? stream) (rx/share)) - mouse (->> (rx/sample 10 ms/mouse-position) - (rx/map #(gpt/divide % (gpt/point zoom)))) + mouse (rx/sample 10 ms/mouse-position) points (->> stream (rx/filter ms/mouse-click?) @@ -277,10 +278,10 @@ (ptk/reify ::handle-drawing-curve ptk/WatchEvent (watch [_ state stream] - (let [{:keys [zoom flags]} (:workspace-local state) + (prn "handle-drawing-curve") + (let [{:keys [flags]} (:workspace-local state) stoper (rx/filter stoper-event? stream) - mouse (->> (rx/sample 10 ms/mouse-position) - (rx/map #(gpt/divide % (gpt/point zoom))))] + mouse (rx/sample 10 ms/mouse-position)] (rx/concat (rx/of initialize-drawing) (->> mouse @@ -324,15 +325,14 @@ (declare path-draw-area) (mf/defc draw-area - [{:keys [zoom shape] :as props}] + [{:keys [shape] :as props}] (when (:id shape) (case (:type shape) (:path :curve) [:& path-draw-area {:shape shape}] - [:& generic-draw-area {:shape shape - :zoom zoom}]))) + [:& generic-draw-area {:shape shape}]))) (mf/defc generic-draw-area - [{:keys [shape zoom]}] + [{:keys [shape]}] (let [{:keys [x y width height]} (geom/selection-rect-shape shape)] (when (and x y) [:g diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index 9e96df06a..d28f2c06f 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -37,13 +37,7 @@ on-zoom-to-100 on-zoom-to-200] :as props}] - (let [show-dropdown? (mf/use-state false) - ;; increase #(st/emit! dv/increase-zoom) - ;; decrease #(st/emit! dv/decrease-zoom) - ;; zoom-to-50 #(st/emit! dv/zoom-to-50) - ;; zoom-to-100 #(st/emit! dv/reset-zoom) - ;; zoom-to-200 #(st/emit! dv/zoom-to-200) - ] + (let [show-dropdown? (mf/use-state false)] [:div.zoom-widget {:on-click #(reset! show-dropdown? true)} [:span {} (str (mth/round (* 100 zoom)) "%")] [:span.dropdown-button i/arrow-down] diff --git a/frontend/src/uxbox/main/ui/workspace/rules.cljs b/frontend/src/uxbox/main/ui/workspace/rules.cljs index 9f31edf8c..3b0d83fba 100644 --- a/frontend/src/uxbox/main/ui/workspace/rules.cljs +++ b/frontend/src/uxbox/main/ui/workspace/rules.cljs @@ -2,156 +2,129 @@ ;; 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) 2015-2017 Andrey Antukh -;; Copyright (c) 2015-2017 Juan de la Cruz +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main.ui.workspace.rules (:require - [beicon.core :as rx] - [cuerdas.core :as str] [rumext.alpha :as mf] - [uxbox.main.constants :as c] - [uxbox.main.refs :as refs] - [uxbox.main.store :as s] - [uxbox.main.streams :as ms] - [uxbox.main.ui.hooks :refer [use-rxsub]] - [uxbox.util.dom :as dom])) + [uxbox.util.object :as obj])) -;; --- Constants & Helpers - -(def rule-padding 20) -(def step-padding 20) -(def step-size 10) -(def scroll-padding 50) - -(def +ticks+ (range 0 c/viewport-width step-size)) - -(defn big-ticks-mod [zoom] (/ 100 zoom)) -(defn mid-ticks-mod [zoom] (/ 50 zoom)) - - - -(defn- make-vertical-tick - [zoom acc value] - (let [big-ticks-mod (big-ticks-mod zoom) - mid-ticks-mod (mid-ticks-mod zoom) - pos (+ (* value zoom) - rule-padding - scroll-padding)] - (cond - (< (mod value big-ticks-mod) step-size) - (conj acc (str/format "M %s %s L %s %s" pos 5 pos step-padding)) - - (< (mod value mid-ticks-mod) step-size) - (conj acc (str/format "M %s %s L %s %s" pos 10 pos step-padding)) - - :else - (conj acc (str/format "M %s %s L %s %s" pos 15 pos step-padding))))) - -(defn- make-horizontal-tick - [zoom acc value] - (let [big-ticks-mod (big-ticks-mod zoom) - mid-ticks-mod (mid-ticks-mod zoom) - pos (+ (* value zoom) - scroll-padding)] - (cond - (< (mod value big-ticks-mod) step-size) - (conj acc (str/format "M %s %s L %s %s" 5 pos step-padding pos)) - - (< (mod value mid-ticks-mod) step-size) - (conj acc (str/format "M %s %s L %s %s" 10 pos step-padding pos)) - - :else - (conj acc (str/format "M %s %s L %s %s" 15 pos step-padding pos))))) - -;; --- Horizontal Text Label - -(mf/defc horizontal-text-label - [{:keys [zoom value] :as props}] - (let [big-ticks-mod (big-ticks-mod zoom) - pos (+ (* value zoom) - rule-padding - scroll-padding)] - (when (< (mod value big-ticks-mod) step-size) - [:text {:x (+ pos 2) - :y 13 - :key (str pos) - :fill "#9da2a6" - :style {:font-size "12px"}} - value]))) - -;; --- Horizontal Text Label - -(mf/defc vertical-text-label - [{:keys [zoom value] :as props}] - (let [big-ticks-mod (big-ticks-mod zoom) - pos (+ (* value zoom) - scroll-padding)] - (when (< (mod value big-ticks-mod) step-size) - [:text {:y (- pos 3) - :x 5 - :key (str pos) - :fill "#9da2a6" - :transform (str/format "rotate(90 0 %s)" pos) - :style {:font-size "12px"}} - value]))) - -;; --- Horizontal Rule Ticks (Component) - -(mf/defc horizontal-rule-ticks - {:wrap [mf/memo]} - [{:keys [zoom]}] - (let [path (reduce (partial make-vertical-tick zoom) [] +ticks+)] - [:g - [:path {:d (str/join " " path)}] - (for [tick +ticks+] - [:& horizontal-text-label {:zoom zoom :value tick :key tick}])])) - -;; --- Vertical Rule Ticks (Component) - -(mf/defc vertical-rule-ticks - {:wrap [mf/memo]} - [{:keys [zoom]}] - (let [path (reduce (partial make-horizontal-tick zoom) [] +ticks+)] - [:g - [:path {:d (str/join " " path)}] - (for [tick +ticks+] - [:& vertical-text-label {:zoom zoom :value tick :key tick}])])) - -;; --- Horizontal Rule (Component) +(def STEP-PADDING 20) (mf/defc horizontal-rule - {:wrap [mf/memo]} - [props] - (let [scroll (use-rxsub ms/viewport-scroll) - zoom (mf/deref refs/selected-zoom) - translate-x (- (- scroll-padding) (:x scroll))] - [:svg.horizontal-rule - {:width c/viewport-width - :height 20} - [:rect {:height 20 - :width c/viewport-width}] - [:g {:transform (str "translate(" translate-x ", 0)")} - [:& horizontal-rule-ticks {:zoom zoom}]]])) + [{:keys [zoom size]}] + (let [canvas (mf/use-ref) + {:keys [x viewport-width width]} size] + + (mf/use-layout-effect + (mf/deps viewport-width width x zoom) + (fn [] + (let [node (mf/ref-val canvas) + dctx (.getContext node "2d") + + btm 1 + trx (- (* (- 0 x) zoom) 50) + + min-val (js/Math.round x) + max-val (js/Math.round (+ x (/ viewport-width zoom))) + + tmp0 (js/Math.abs (- max-val min-val)) + tmp1 (js/Math.round (/ tmp0 200)) + btm (max (* btm (* 10 tmp1)) 1)] + + (obj/set! node "width" viewport-width) + + (obj/set! dctx "fillStyle" "#E8E9EA") + (.fillRect dctx 0 0 viewport-width 20) + + (.save dctx) + (.translate dctx trx 0) + + (obj/set! dctx "font" "12px serif") + (obj/set! dctx "fillStyle" "#7B7D85") + (obj/set! dctx "strokeStyle" "#7B7D85") + (obj/set! dctx "textAlign" "center") + + (loop [i min-val] + (when (< i max-val) + (let [pos (+ (* i zoom) 50)] + (when (= (mod i btm) 0) + (.fillText dctx (str i) (- pos 0) 13)) + (recur (+ i 1))))) + + (let [path (js/Path2D.)] + (loop [i min-val] + (if (> i max-val) + (.stroke dctx path) + (let [pos (+ (* i zoom) 50)] + (when (= (mod i btm) 0) + (.moveTo path pos 17) + (.lineTo path pos STEP-PADDING)) + (recur (inc i)))))) + + (.restore dctx)))) + + [:canvas.horizontal-rule {:ref canvas :width (:viewport-width size) :height 20}])) + ;; --- Vertical Rule (Component) (mf/defc vertical-rule - {:wrap [mf/memo]} - [props] - (let [scroll (use-rxsub ms/viewport-scroll) - zoom (or (mf/deref refs/selected-zoom) 1) - scroll-y (:y scroll) - translate-y (+ (- scroll-padding) - (- (:y scroll))) - ] - [:svg.vertical-rule {:width 20 - ;; :x 0 :y 0 - :height c/viewport-height} + {::mf/wrap [mf/memo #(mf/throttle % 60)]} + [{:keys [zoom size]}] + (let [canvas (mf/use-ref) + {:keys [y height viewport-height]} size] + (mf/use-layout-effect + (mf/deps height y zoom) + (fn [] + (let [node (mf/ref-val canvas) + dctx (.getContext node "2d") + + btm 1 + try (- (* (- 0 y) zoom) 50) + + min-val (js/Math.round y) + max-val (js/Math.round (+ y (/ viewport-height zoom))) + + tmp0 (js/Math.abs (- max-val min-val)) + tmp1 (js/Math.round (/ tmp0 100)) + btm (max (* btm (* 10 tmp1)) 1)] + + (obj/set! node "height" viewport-height) + + (obj/set! dctx "fillStyle" "#E8E9EA") + (.fillRect dctx 0 0 20 viewport-height) + + (obj/set! dctx "font" "11px serif") + (obj/set! dctx "fillStyle" "#7B7D85") + (obj/set! dctx "strokeStyle" "#7B7D85") + (obj/set! dctx "textAlign" "center") + + (.translate dctx 0 try) + + (loop [i min-val] + (when (< i max-val) + (let [pos (+ (* i zoom) 50)] + (when (= (mod i btm) 0) + (.save dctx) + (.translate dctx 12 pos) + (.rotate dctx (/ (* 270 js/Math.PI) 180)) + (.fillText dctx (str i) 0 0) + (.restore dctx)) + (recur (inc i))))) + + (let [path (js/Path2D.)] + (loop [i min-val] + (if (> i max-val) + (.stroke dctx path) + (let [pos (+ (* i zoom) 50)] + (when (= (mod i btm) 0) + (.moveTo path 17 pos) + (.lineTo path STEP-PADDING pos)) + (recur (inc i))))))))) + + [:canvas.vertical-rule {:ref canvas :width 20 :height height}])) - [:g {:transform (str "translate(0, " (+ translate-y 20) ")")} - [:& vertical-rule-ticks {:zoom zoom}]] - [:rect {:x 0 - :y 0 - :height 20 - :width 20}]])) diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index ff76e2ca7..56094fc2e 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -9,9 +9,9 @@ (ns uxbox.main.ui.workspace.viewport (:require + [cuerdas.core :as str] [beicon.core :as rx] [goog.events :as events] - [lentes.core :as l] [potok.core :as ptk] [rumext.alpha :as mf] [uxbox.main.ui.icons :as i] @@ -30,6 +30,7 @@ [uxbox.main.ui.workspace.selection :refer [selection-handlers]] [uxbox.main.ui.workspace.presence :as presence] [uxbox.main.ui.workspace.snap-feedback :refer [snap-feedback]] + [uxbox.util.math :as mth] [uxbox.util.dom :as dom] [uxbox.util.geom.point :as gpt] [uxbox.util.perf :as perf] @@ -41,7 +42,6 @@ (mf/defc coordinates [{:keys [zoom] :as props}] (let [coords (some-> (hooks/use-rxsub ms/mouse-position) - (gpt/divide (gpt/point zoom zoom)) (gpt/round 0))] [:ul.coordinates [:span {:alt "x"} @@ -87,22 +87,21 @@ ;; --- Viewport Positioning -(def handle-viewport-positioning - (letfn [(on-point [dom reference point] - (let [{:keys [x y]} (gpt/subtract point reference) - cx (.-scrollLeft dom) - cy (.-scrollTop dom)] - (set! (.-scrollLeft dom) (- cx x)) - (set! (.-scrollTop dom) (- cy y))))] - (ptk/reify ::handle-viewport-positioning - ptk/EffectEvent - (effect [_ state stream] - (let [stoper (rx/filter #(= ::finish-positioning %) stream) - reference @ms/mouse-position - dom (dom/get-element "workspace-viewport")] - (-> (rx/take-until stoper ms/mouse-position) - (rx/subscribe #(on-point dom reference %)))))))) +(defn- handle-viewport-positioning + [viewport-ref] + (let [node (mf/ref-val viewport-ref) + stoper (rx/filter #(= ::finish-positioning %) st/stream) + stream (->> ms/mouse-position-delta + (rx/take-until stoper))] + (rx/subscribe stream + (fn [delta] + (let [vbox (.. ^js node -viewBox -baseVal) + zoom (gpt/point @refs/selected-zoom) + delta (gpt/divide delta zoom)] + (st/emit! (dw/update-viewport-position + {:x #(- % (:x delta)) + :y #(- % (:y delta))}))))))) ;; --- Viewport @@ -125,16 +124,16 @@ [:& shape-wrapper {:shape item :key (:id item)}]))])) - (mf/defc viewport - [{:keys [page] :as props}] - (let [{:keys [drawing-tool + [{:keys [page local] :as props}] + (let [ + {:keys [drawing-tool zoom flags + size edition tooltip - selected] - :as local} (mf/deref refs/workspace-local) + selected]} local viewport-ref (mf/use-ref nil) last-position (mf/use-var nil) @@ -146,16 +145,25 @@ (mf/deps drawing-tool edition) (fn [event] (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) + (let [event (.-nativeEvent event) + ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) opts {:shift? shift? :ctrl? ctrl?}] (st/emit! (ms/->MouseEvent :down ctrl? shift?)) - (when (and (not edition) - (= 1 (.-which (.-nativeEvent event)))) + + (cond + (and (not edition) (= 1 (.-which event))) (if drawing-tool (st/emit! (start-drawing drawing-tool)) - (st/emit! dw/handle-selection)))))) + (st/emit! dw/handle-selection)) + + (and (not edition) + (= 2 (.-which event))) + (handle-viewport-positioning viewport-ref) + + :else + (js/console.log "on-mouse-down" event))))) on-context-menu (mf/use-callback @@ -169,11 +177,15 @@ (mf/use-callback (fn [event] (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) + (let [event (.-nativeEvent event) + ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) opts {:shift? shift? :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :up ctrl? shift?))))) + (st/emit! (ms/->MouseEvent :up ctrl? shift?)) + + (when (= 2 (.-which event)) + (st/emit! ::finish-positioning))))) on-click (mf/use-callback @@ -208,7 +220,7 @@ (when-not (.-repeat bevent) (st/emit! (ms/->KeyboardEvent :down key ctrl? shift?)) (when (kbd/space? event) - (st/emit! handle-viewport-positioning)))))) + (handle-viewport-positioning viewport-ref)))))) on-key-up (mf/use-callback @@ -220,21 +232,33 @@ :shift? shift? :ctrl? ctrl?}] (when (kbd/space? event) - (st/emit! ::finish-positioning #_(dw/stop-viewport-positioning))) + (st/emit! ::finish-positioning)) (st/emit! (ms/->KeyboardEvent :up key ctrl? shift?))))) translate-point-to-viewport (fn [pt] (let [viewport (mf/ref-val viewport-ref) + vbox (.. ^js viewport -viewBox -baseVal) brect (.getBoundingClientRect viewport) brect (gpt/point (d/parse-integer (.-left brect)) - (d/parse-integer (.-top brect)))] - (gpt/subtract pt brect))) + (d/parse-integer (.-top brect))) + box (gpt/point (.-x vbox) + (.-y vbox)) + ] + (-> (gpt/subtract pt brect) + (gpt/divide (gpt/point @refs/selected-zoom)) + (gpt/add box)))) on-mouse-move (fn [event] - (let [pt (gpt/point (.-clientX event) (.-clientY event)) - pt (translate-point-to-viewport pt)] + (let [event (.getBrowserEvent event) + pt (gpt/point (.-clientX event) (.-clientY event)) + pt (translate-point-to-viewport pt) + delta (gpt/point (.-movementX event) + (.-movementY event))] + (st/emit! (ms/->PointerEvent :delta delta + (kbd/ctrl? event) + (kbd/shift? event))) (st/emit! (ms/->PointerEvent :viewport pt (kbd/ctrl? event) (kbd/shift? event))))) @@ -242,30 +266,20 @@ on-mouse-wheel (mf/use-callback (fn [event] - (when (kbd/ctrl? event) - ;; Disable browser zoom with ctrl+mouse wheel - (dom/prevent-default event) - (let [event (.getBrowserEvent event)] - (if (pos? (.-deltaY event)) - (st/emit! dw/decrease-zoom) - (st/emit! dw/increase-zoom)))))) + (dom/prevent-default event) + (dom/stop-propagation event) - on-mount - (fn [] - (let [key1 (events/listen js/document EventType.KEYDOWN on-key-down) - key2 (events/listen js/document EventType.KEYUP on-key-up) - dnode (mf/ref-val viewport-ref) - key3 (events/listen dnode EventType.MOUSEMOVE on-mouse-move) - ;; bind with passive=false to allow the event to be cancelled - ;; https://stackoverflow.com/a/57582286/3219895 - key4 (events/listen js/window EventType.WHEEL on-mouse-wheel - #js {"passive" false})] - (fn [] - (events/unlistenByKey key1) - (events/unlistenByKey key2) - (events/unlistenByKey key3) - (events/unlistenByKey key4) - ))) + (if (kbd/ctrl? event) + (let [event (.getBrowserEvent event) + pos @ms/mouse-position] + (if (pos? (.-deltaY event)) + (st/emit! (dw/decrease-zoom pos)) + (st/emit! (dw/increase-zoom pos)))) + (let [event (.getBrowserEvent event) + delta (.-deltaY ^js event)] + (if (kbd/shift? event) + (st/emit! (dw/update-viewport-position {:x #(+ % delta)})) + (st/emit! (dw/update-viewport-position {:y #(+ % delta)}))))))) on-drag-over ;; Should prevent only events that we'll handle on-drop @@ -280,13 +294,54 @@ final-y (- (:y viewport-coord) (/ (:height shape) 2))] (st/emit! (dw/add-shape (-> shape (assoc :x final-x) - (assoc :y final-y))))))] + (assoc :y final-y)))))) + + on-resize + (fn [event] + (let [node (mf/ref-val viewport-ref) + parent (.-parentElement ^js node)] + (st/emit! (dw/update-viewport-size + {:width (.-clientWidth ^js parent) + :height (.-clientHeight ^js parent)})))) + + on-mount + (fn [] + (let [node (mf/ref-val viewport-ref) + prnt (.-parentElement ^js node) + + key1 (events/listen js/document EventType.KEYDOWN on-key-down) + key2 (events/listen js/document EventType.KEYUP on-key-up) + key3 (events/listen node EventType.MOUSEMOVE on-mouse-move) + ;; bind with passive=false to allow the event to be cancelled + ;; https://stackoverflow.com/a/57582286/3219895 + key4 (events/listen js/window EventType.WHEEL on-mouse-wheel + #js {"passive" false}) + key5 (events/listen js/window EventType.RESIZE on-resize)] + + (st/emit! (dw/initialize-viewport + {:width (.-clientWidth ^js prnt) + :height (.-clientHeight ^js prnt)})) + + (fn [] + (events/unlistenByKey key1) + (events/unlistenByKey key2) + (events/unlistenByKey key3) + (events/unlistenByKey key4) + (events/unlistenByKey key5) + ))) + + ] (mf/use-effect on-mount) [:* [:& coordinates {:zoom zoom}] - [:svg.viewport {:width (* c/viewport-width zoom) - :height (* c/viewport-height zoom) + [:svg.viewport { + :width (:viewport-width size) + :height (:viewport-height size) + :view-box (str/join " " [(:x size) + (:y size) + (:width size) + (:height size)]) :ref viewport-ref :class (when drawing-tool "drawing") :on-context-menu on-context-menu @@ -296,9 +351,7 @@ :on-mouse-up on-mouse-up :on-drag-over on-drag-over :on-drop on-drop} - - [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} - ;; [:& perf/profiler {:label "viewport-frames"} + [:g [:& frames {:key (:id page)}] (when (seq selected) @@ -314,7 +367,7 @@ [:& snap-feedback] - (if (contains? flags :grid) + (when (contains? flags :grid) [:& grid])] (when tooltip @@ -325,3 +378,4 @@ [:& presence/active-cursors {:page page}] [:& selection-rect {:data (:selrect local)}]]])) + diff --git a/frontend/src/uxbox/util/geom/shapes.cljs b/frontend/src/uxbox/util/geom/shapes.cljs index 0cd87dd92..dbc5190e6 100644 --- a/frontend/src/uxbox/util/geom/shapes.cljs +++ b/frontend/src/uxbox/util/geom/shapes.cljs @@ -594,14 +594,6 @@ ;; --- Helpers -(defn apply-zoom - [selrect zoom] - (assoc selrect - :x (/ (:x selrect) (:x zoom)) - :y (/ (:y selrect) (:y zoom)) - :width (/ (:width selrect) (:x zoom)) - :height (/ (:height selrect) (:y zoom)))) - (defn contained-in? "Check if a shape is contained in the provided selection rect."