(ns uxbox.ui.workspace.canvas (:require [sablono.core :as html :refer-macros [html]] [rum.core :as rum] [beicon.core :as rx] [cats.labs.lens :as l] [goog.events :as events] [uxbox.router :as r] [uxbox.rstore :as rs] [uxbox.state :as st] [uxbox.shapes :as shapes] [uxbox.util.lens :as ul] [uxbox.library.icons :as _icons] [uxbox.data.projects :as dp] [uxbox.data.workspace :as dw] [uxbox.ui.mixins :as mx] [uxbox.ui.dom :as dom] [uxbox.ui.util :as util] [uxbox.ui.workspace.base :as wb] [uxbox.ui.workspace.grid :refer (grid)] [uxbox.ui.workspace.options :refer (element-opts)]) (:import goog.events.EventType)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Lenses ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:static shapes-by-id (as-> (l/key :shapes-by-id) $ (l/focus-atom $ st/state))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Background ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn background-render [] (html [:rect {:x 0 :y 0 :width "100%" :height "100%" :fill "white"}])) (def background (util/component {:render background-render :name "background" :mixins [mx/static]})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Select Rect ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn selrect-render [own] (when-let [data (rum/react wb/selrect-pos)] (let [{:keys [x y width height]} (wb/selrect->rect data)] (html [:rect.selection-rect {:x x :y y :width width :height height}])))) (def ^:static selrect (util/component {:render selrect-render :name "selrect" :mixins [mx/static rum/reactive]})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Selected shapes group ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def ^:private selection-circle-style {:fillOpacity "0.5" :strokeWidth "1px" :vectorEffect "non-scaling-stroke"}) (def ^:private default-selection-props {:r 4 :style selection-circle-style :fill "lavender" :stroke "gray"}) (defn- selected-shapes-render [own shapes selected] (let [local (:rum/local own) x (apply min (map :x shapes)) y (apply min (map :y shapes)) x' (apply max (map (fn [{:keys [x width]}] (+ x width)) shapes)) y' (apply max (map (fn [{:keys [y height]}] (+ y height)) shapes)) width (- x' x) height (- y' y)] (letfn [(on-mouse-down [event] (dom/stop-propagation event) (swap! local assoc :init-coords [x y]) (reset! wb/shapes-dragging? true)) (on-mouse-up [event] (dom/stop-propagation event) (reset! wb/shapes-dragging? false))] (html [:g {:class "shape selected" :on-mouse-down on-mouse-down :on-mouse-up on-mouse-up} (for [item shapes] (shapes/render item)) [:g.controls [:rect {:x x :y y :width width :height height :style {:stroke "black" :fill "transparent" :stroke-opacity "0.5"}}] [:circle.top-left (merge default-selection-props {:cx x :cy y})] [:circle.top-right (merge default-selection-props {:cx (+ x width) :cy y})] [:circle.bottom-left (merge default-selection-props {:cx x :cy (+ y height)})] [:circle.bottom-right (merge default-selection-props {:cx (+ x width) :cy (+ y height)})]]])))) (def selected-shapes (util/component {:render selected-shapes-render :name "selected-shapes" :mixins [mx/static rum/reactive (mx/local {})]})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shape ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn shape-render [own shape selected] (let [{:keys [id x y width height] :as shape} shape selected? (contains? selected id)] (letfn [(on-mouse-down [event] (let [local (:rum/local own)] (dom/stop-propagation event) (swap! local assoc :init-coords [x y]) (reset! wb/shapes-dragging? true)) (cond (and (not selected?) (empty? selected)) (rs/emit! (dw/select-shape id)) (and (not selected?) (not (empty? selected))) (if (.-ctrlKey event) (rs/emit! (dw/select-shape id)) (rs/emit! (dw/deselect-all) (dw/select-shape id))))) (on-mouse-up [event] (dom/stop-propagation event) (reset! wb/shapes-dragging? false))] (html [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down :on-mouse-up on-mouse-up} (shapes/render shape) (if selected? [:g.controls [:rect {:x x :y y :width width :height height :style {:stroke "black" :fill "transparent" :stroke-opacity "0.5"}}] [:circle.top-left (merge default-selection-props {:cx x :cy y})] [:circle.top-right (merge default-selection-props {:cx (+ x width) :cy y})] [:circle.bottom-left (merge default-selection-props {:cx x :cy (+ y height)})] [:circle.bottom-right (merge default-selection-props {:cx (+ x width) :cy (+ y height)})]])])))) (def shape (util/component {:render shape-render :name "shape" :mixins [mx/static (mx/local {})]})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Canvas ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- canvas-did-mount [own] (letfn [(on-mousemove [event page [offset-x offset-y]] (let [x (.-clientX event) y (.-clientY event) event {:id (:id page) :coords [(- x offset-x) (- y offset-y)]}] (rx/push! wb/mouse-b event)))] (let [[page] (:rum/props own) canvas (util/get-ref-dom own (str "canvas" (:id page))) brect (.getBoundingClientRect canvas) brect [(.-left brect) (.-top brect)] key (events/listen js/document EventType.MOUSEMOVE #(on-mousemove % page brect))] (swap! wb/bounding-rect assoc (:id page) brect) (assoc own ::eventkey key)))) (defn- canvas-will-unmount [own] (let [key (::eventkey own) [page] (:rum/props own)] (swap! wb/bounding-rect dissoc (:id page)) (events/unlistenByKey key) (dissoc own ::eventkey))) (defn- canvas-transfer-state [old-own own] (let [key (::eventkey old-own)] (assoc own ::eventkey key))) (defn- canvas-render [own {:keys [width height id] :as page}] (let [workspace (rum/react wb/workspace-state) shapes-by-id (rum/react shapes-by-id) workspace-selected (:selected workspace) shapes (map #(get shapes-by-id %) (:shapes page)) shapes-selected (filter (comp workspace-selected :id) shapes) shapes-notselected (filter (comp not workspace-selected :id) shapes)] (letfn [(on-mouse-down [event] (dom/stop-propagation event) (when-not (empty? shapes-selected) (rs/emit! (dw/deselect-all))) (reset! wb/selrect-dragging? true)) (on-mouse-up [event] (dom/stop-propagation event) (reset! wb/shapes-dragging? false) (reset! wb/selrect-dragging? false))] (html [:svg#page-canvas.page-canvas {:x wb/document-start-x :y wb/document-start-y :ref (str "canvas" id) :width width :height height :on-mouse-down on-mouse-down :on-mouse-up on-mouse-up} (background) (grid 1) [:svg.page-layout {} (for [item shapes-notselected :let [component (shape item workspace-selected)]] (rum/with-key component (str (:id item)))) (cond (= (count shapes-selected) 1) (let [item (first shapes-selected)] (shape item workspace-selected)) (> (count shapes-selected) 1) (selected-shapes shapes-selected)) (selrect)]])))) (def canvas (util/component {:render canvas-render :did-mount canvas-did-mount :will-unmount canvas-will-unmount :transfer-state canvas-transfer-state :name "canvas" :mixins [mx/static rum/reactive]}))