penpot/frontend/src/uxbox/main/ui/workspace/selection.cljs
2020-06-04 15:07:43 +02:00

334 lines
12 KiB
Clojure

;; 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/.
;;
;; 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.selection
"Selection handlers component."
(:require
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]
[rumext.alpha :as mf]
[rumext.util :refer [map->obj]]
[uxbox.main.data.workspace :as dw]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.streams :as ms]
[uxbox.main.ui.cursors :as cur]
[uxbox.util.dom :as dom]
[uxbox.util.object :as obj]
[uxbox.common.geom.shapes :as geom]
[uxbox.common.geom.point :as gpt]
[uxbox.common.geom.matrix :as gmt]
[uxbox.util.debug :refer [debug?]]
[uxbox.main.ui.workspace.shapes.outline :refer [outline]]))
(def rotation-handler-size 25)
(def resize-point-radius 4)
(def resize-point-circle-radius 10)
(def resize-point-rect-size 20)
(def resize-side-height 8)
(def selection-rect-color "#1FDEA7")
(def selection-rect-width 1)
(mf/defc selection-rect [{:keys [transform rect zoom]}]
(let [{:keys [x y width height]} rect]
[:rect.main
{:x x
:y y
:width width
:height height
:transform transform
:style {:stroke selection-rect-color
:stroke-width (/ selection-rect-width zoom)
:fill "transparent"}}]))
(defn- handlers-for-selection [{:keys [x y width height]}]
[;; TOP-LEFT
{:type :rotation
:position :top-left
:props {:cx x :cy y}}
{:type :resize-point
:position :top-left
:props {:cx x :cy y}}
;; TOP
{:type :resize-side
:position :top
:props {:x x :y y :length width :angle 0 }}
;; TOP-RIGHT
{:type :rotation
:position :top-right
:props {:cx (+ x width) :cy y}}
{:type :resize-point
:position :top-right
:props {:cx (+ x width) :cy y}}
;; RIGHT
{:type :resize-side
:position :right
:props {:x (+ x width) :y y :length height :angle 90 }}
;; BOTTOM-RIGHT
{:type :rotation
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
{:type :resize-point
:position :bottom-right
:props {:cx (+ x width) :cy (+ y height)}}
;; BOTTOM
{:type :resize-side
:position :bottom
:props {:x (+ x width) :y (+ y height) :length width :angle 180 }}
;; BOTTOM-LEFT
{:type :rotation
:position :bottom-left
:props {:cx x :cy (+ y height)}}
{:type :resize-point
:position :bottom-left
:props {:cx x :cy (+ y height)}}
;; LEFT
{:type :resize-side
:position :left
:props {:x x :y (+ y height) :length height :angle 270 }}])
(mf/defc rotation-handler [{:keys [cx cy transform position rotation zoom on-rotate]}]
(let [size (/ rotation-handler-size zoom)
x (- cx (if (#{:top-left :bottom-left} position) size 0))
y (- cy (if (#{:top-left :top-right} position) size 0))
angle (case position
:top-left 0
:top-right 90
:bottom-right 180
:bottom-left 270)]
[:rect {:style {:cursor (cur/rotate (+ rotation angle))}
:x x
:y y
:width size
:height size
:fill (if (debug? :rotation-handler) "blue" "transparent")
:transform transform
:on-mouse-down on-rotate}]))
(mf/defc resize-point-handler [{:keys [cx cy zoom position on-resize transform rotation]}]
(let [{cx' :x cy' :y} (gpt/transform (gpt/point cx cy) transform)
rot-square (case position
:top-left 0
:top-right 90
:bottom-right 180
:bottom-left 270)]
[:g.resize-handler
[:circle {:r (/ resize-point-radius zoom)
:style {:fillOpacity "1"
:strokeWidth "1px"
:vectorEffect "non-scaling-stroke"
}
:fill "#FFFFFF"
:stroke "#1FDEA7"
:cx cx'
:cy cy'}]
[:rect {:x cx
:y cy
:width (/ resize-point-rect-size zoom)
:height (/ resize-point-rect-size zoom)
:fill (if (debug? :resize-handler) "red" "transparent")
:on-mouse-down on-resize
:style {:cursor (if (#{:top-left :bottom-right} position)
(cur/resize-nesw rotation) (cur/resize-nwse rotation))}
:transform (gmt/multiply transform
(gmt/rotate-matrix rot-square (gpt/point cx cy)))}]
[:circle {:on-mouse-down on-resize
:r (/ resize-point-circle-radius zoom)
:fill (if (debug? :resize-handler) "red" "transparent")
:cx cx'
:cy cy'
:style {:cursor (if (#{:top-left :bottom-right} position)
(cur/resize-nesw rotation) (cur/resize-nwse rotation))}}]
]))
(mf/defc resize-side-handler [{:keys [x y length angle zoom position rotation transform on-resize]}]
[:rect {:x (+ x (/ resize-point-rect-size zoom))
:y (- y (/ resize-side-height 2 zoom))
:width (max 0 (- length (/ (* resize-point-rect-size 2) zoom)))
:height (/ resize-side-height zoom)
:transform (gmt/multiply transform
(gmt/rotate-matrix angle (gpt/point x y)))
:on-mouse-down on-resize
:style {:fill (if (debug? :resize-handler) "yellow" "transparent")
:cursor (if (#{:left :right} position)
(cur/resize-ew rotation)
(cur/resize-ns rotation)) }}])
(mf/defc controls
{::mf/wrap-props false}
[props]
(let [shape (obj/get props "shape")
zoom (obj/get props "zoom")
on-resize (obj/get props "on-resize")
on-rotate (obj/get props "on-rotate")
current-transform (mf/deref refs/current-transform)
selrect (geom/shape->rect-shape shape)
transform (geom/transform-matrix shape)]
(when (not (#{:move :rotate} current-transform))
[:g.controls
;; Selection rect
[:& selection-rect {:rect selrect
:transform transform
:zoom zoom}]
[:& outline {:shape (geom/transform-shape shape)}]
;; Handlers
(for [{:keys [type position props]} (handlers-for-selection selrect)]
(let [common-props {:key (str (name type) "-" (name position))
:zoom zoom
:position position
:on-rotate on-rotate
:on-resize (partial on-resize position)
:transform transform
:rotation (:rotation shape)}
props (->> props (merge common-props) map->obj)]
(case type
:rotation (when (not= :frame (:type shape)) [:> rotation-handler props])
:resize-point [:> resize-point-handler props]
:resize-side [:> resize-side-handler props])))])))
;; --- Selection Handlers (Component)
(mf/defc path-edition-selection-handlers
[{:keys [shape modifiers zoom] :as props}]
(letfn [(on-mouse-down [event index]
(dom/stop-propagation event)
;; TODO: this need code ux refactor
(let [stoper (get-edition-stream-stoper)
stream (->> (ms/mouse-position-deltas @ms/mouse-position)
(rx/take-until stoper))]
;; (when @refs/selected-alignment
;; (st/emit! (dw/initial-path-point-align (:id shape) index)))
(rx/subscribe stream #(on-handler-move % index))))
(get-edition-stream-stoper []
(let [stoper? #(and (ms/mouse-event? %) (= (:type %) :up))]
(rx/merge
(rx/filter stoper? st/stream)
(->> st/stream
(rx/filter #(= % :interrupt))
(rx/take 1)))))
(on-handler-move [delta index]
(st/emit! (dw/update-path (:id shape) index delta)))]
(let [transform (geom/transform-matrix shape)
displacement (:displacement modifiers)
segments (cond->> (:segments shape)
displacement (map #(gpt/transform % displacement)))]
[:g.controls
(for [[index {:keys [x y]}] (map-indexed vector segments)]
(let [{:keys [x y]} (gpt/transform (gpt/point x y) transform)]
[:circle {:cx x :cy y
:r (/ 6.0 zoom)
:key index
:on-mouse-down #(on-mouse-down % index)
:fill "#ffffff"
:stroke "#1FDEA7"
:style {:cursor cur/move-pointer}}]))])))
;; TODO: add specs for clarity
(mf/defc text-edition-selection-handlers
[{:keys [shape zoom] :as props}]
(let [{:keys [x y width height]} shape]
[:g.controls
[:rect.main {:x x :y y
:transform (geom/transform-matrix shape)
:width width
:height height
:style {:stroke "#1FDEA7"
:stroke-width "0.5"
:stroke-opacity "1"
:fill "transparent"}}]]))
(mf/defc multiple-selection-handlers
[{:keys [shapes selected zoom] :as props}]
(let [shape (geom/selection-rect shapes)
shape-center (geom/center shape)
on-resize #(do (dom/stop-propagation %2)
(st/emit! (dw/start-resize %1 selected shape)))
on-rotate #(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate shapes)))]
[:*
[:& controls {:shape shape
:zoom zoom
:on-resize on-resize
:on-rotate on-rotate}]
(when (debug? :selection-center)
[:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}])]))
(mf/defc single-selection-handlers
[{:keys [shape zoom] :as props}]
(let [shape-id (:id shape)
shape (geom/transform-shape shape)
shape' (if (debug? :simple-selection) (geom/selection-rect [shape]) shape)
on-resize
#(do (dom/stop-propagation %2)
(st/emit! (dw/start-resize %1 #{shape-id} shape')))
on-rotate
#(do (dom/stop-propagation %)
(st/emit! (dw/start-rotate [shape])))]
[:*
[:& controls {:shape shape'
:zoom zoom
:on-rotate on-rotate
:on-resize on-resize}]]))
(mf/defc selection-handlers
[{:keys [selected edition zoom] :as props}]
(let [;; We need remove posible nil values because on shape
;; deletion many shape will reamin selected and deleted
;; in the same time for small instant of time
shapes (->> (mf/deref (refs/objects-by-id selected))
(remove nil?))
num (count shapes)
{:keys [id type] :as shape} (first shapes)]
(cond
(zero? num)
nil
(> num 1)
[:& multiple-selection-handlers {:shapes shapes
:selected selected
:zoom zoom}]
(and (= type :text)
(= edition (:id shape)))
[:& text-edition-selection-handlers {:shape shape
:zoom zoom}]
(and (or (= type :path)
(= type :curve))
(= edition (:id shape)))
[:& path-edition-selection-handlers {:shape shape
:zoom zoom}]
:else
[:& single-selection-handlers {:shape shape
:zoom zoom}])))