penpot/frontend/src/app/main/data/workspace/interactions.cljs
2022-03-10 15:37:10 +01:00

322 lines
11 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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.data.workspace.interactions
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.spec :as us]
[app.common.spec.interactions :as csi]
[app.common.spec.page :as csp]
[app.common.uuid :as uuid]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.streams :as ms]
[beicon.core :as rx]
[potok.core :as ptk]))
;; --- Flows
(defn add-flow
[starting-frame]
(ptk/reify ::add-flow
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
flows (get-in page [:options :flows] [])
unames (into #{} (map :name flows))
name (dwc/generate-unique-name unames "Flow-1")
new-flow {:id (uuid/next)
:name name
:starting-frame starting-frame}]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :flows
(csp/add-flow flows new-flow)))))))))
(defn add-flow-selected-frame
[]
(ptk/reify ::add-flow-selected-frame
ptk/WatchEvent
(watch [_ state _]
(let [selected (wsh/lookup-selected state)]
(rx/of (add-flow (first selected)))))))
(defn remove-flow
[flow-id]
(us/verify ::us/uuid flow-id)
(ptk/reify ::remove-flow
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
flows (get-in page [:options :flows] [])]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :flows
(csp/remove-flow flows flow-id)))))))))
(defn rename-flow
[flow-id name]
(us/verify ::us/uuid flow-id)
(us/verify ::us/string name)
(ptk/reify ::rename-flow
ptk/WatchEvent
(watch [it state _]
(let [page (wsh/lookup-page state)
flows (get-in page [:options :flows] [])]
(rx/of (dch/commit-changes
(-> (pcb/empty-changes it)
(pcb/with-page page)
(pcb/set-page-option :flows
(csp/update-flow flows flow-id
#(csp/rename-flow % name))))))))))
(defn start-rename-flow
[id]
(us/verify ::us/uuid id)
(ptk/reify ::start-rename-flow
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :flow-for-rename] id))))
(defn end-rename-flow
[]
(ptk/reify ::end-rename-flow
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :flow-for-rename))))
;; --- Interactions
(defn- connected-frame?
"Check if some frame is origin or destination of any navigate interaction
in the page"
[objects frame-id]
(let [children (cph/get-children-with-self objects frame-id)]
(or (some csi/flow-origin? (map :interactions children))
(some #(csi/flow-to? % frame-id) (map :interactions (vals objects))))))
(defn add-new-interaction
([shape] (add-new-interaction shape nil))
([shape destination]
(ptk/reify ::add-new-interaction
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
frame (cph/get-frame objects shape)
flows (get-in state [:workspace-data
:pages-index
page-id
:options
:flows] [])
flow (csp/get-frame-flow flows (:id frame))]
(rx/concat
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(let [new-interaction (csi/set-destination
csi/default-interaction
destination)]
(update shape :interactions
csi/add-interaction new-interaction)))))
(when (and (not (connected-frame? objects (:id frame)))
(nil? flow))
(rx/of (add-flow (:id frame))))))))))
(defn remove-interaction
[shape index]
(ptk/reify ::remove-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
csi/remove-interaction index)))))))
(defn remove-interactions
[ids]
(ptk/reify ::remove-interactions
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes ids
(fn [shape]
(assoc shape :interactions [])))))))
(defn update-interaction
[shape index update-fn]
(ptk/reify ::update-interaction
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes [(:id shape)]
(fn [shape]
(update shape :interactions
csi/update-interaction index update-fn)))))))
(declare move-edit-interaction)
(declare finish-edit-interaction)
(defn start-edit-interaction
[index]
(ptk/reify ::start-edit-interaction
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :editing-interaction-index] index))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-edit-interaction initial-pos %)))
(rx/of (finish-edit-interaction index initial-pos))))))))
(defn move-edit-interaction
[initial-pos position]
(ptk/reify ::move-edit-interaction
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
selected-shape-id (-> state wsh/lookup-selected first)
selected-shape (get objects selected-shape-id)
selected-shape-frame-id (:frame-id selected-shape)
start-frame (get objects selected-shape-frame-id)
end-frame (dwc/get-frame-at-point objects position)]
(cond-> state
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
(defn finish-edit-interaction
[index initial-pos]
(ptk/reify ::finish-edit-interaction
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :editing-interaction-index] nil)
(assoc-in [:workspace-local :draw-interaction-to] nil)
(assoc-in [:workspace-local :draw-interaction-to-frame] nil)))
ptk/WatchEvent
(watch [_ state _]
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
frame (dwc/get-frame-at-point objects position)
shape-id (-> state wsh/lookup-selected first)
shape (get objects shape-id)]
(when (and shape (not (= position initial-pos)))
(if (nil? frame)
(when index
(rx/of (remove-interaction shape index)))
(let [frame (if (or (= (:id frame) (:id shape))
(= (:id frame) (:frame-id shape)))
nil ;; Drop onto self frame -> set destination to none
frame)]
(if (nil? index)
(rx/of (add-new-interaction shape (:id frame)))
(rx/of (update-interaction shape index
(fn [interaction]
(cond-> interaction
(not (csi/has-destination interaction))
(csi/set-action-type :navigate)
:always
(csi/set-destination (:id frame))))))))))))))
;; --- Overlays
(declare move-overlay-pos)
(declare finish-move-overlay-pos)
(defn start-move-overlay-pos
[index]
(ptk/reify ::start-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:workspace-local :move-overlay-to] nil)
(assoc-in [:workspace-local :move-overlay-index] index)))
ptk/WatchEvent
(watch [_ state stream]
(let [initial-pos @ms/mouse-position
selected (wsh/lookup-selected state)
stopper (rx/filter ms/mouse-up? stream)]
(when (= 1 (count selected))
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
overlay-pos (-> shape
(get-in [:interactions index])
:overlay-position)
orig-frame (cph/get-frame objects shape)
frame-pos (gpt/point (:x orig-frame) (:y orig-frame))
offset (-> initial-pos
(gpt/subtract overlay-pos)
(gpt/subtract frame-pos))]
(rx/concat
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(move-overlay-pos % frame-pos offset)))
(rx/of (finish-move-overlay-pos index frame-pos offset)))))))))
(defn move-overlay-pos
[pos frame-pos offset]
(ptk/reify ::move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(let [pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))]
(assoc-in state [:workspace-local :move-overlay-to] pos)))))
(defn finish-move-overlay-pos
[index frame-pos offset]
(ptk/reify ::finish-move-overlay-pos
ptk/UpdateEvent
(update [_ state]
(-> state
(d/dissoc-in [:workspace-local :move-overlay-to])
(d/dissoc-in [:workspace-local :move-overlay-index])))
ptk/WatchEvent
(watch [_ state _]
(let [pos @ms/mouse-position
overlay-pos (-> pos
(gpt/subtract frame-pos)
(gpt/subtract offset))
page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
shape (->> state
wsh/lookup-selected
first
(get objects))
interactions (:interactions shape)
new-interactions
(update interactions index
#(csi/set-overlay-position % overlay-pos))]
(rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions})))))))