mirror of
https://github.com/penpot/penpot.git
synced 2026-05-05 16:18:35 +00:00
444 lines
16 KiB
Clojure
444 lines
16 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) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace.shape-layout
|
|
(:require
|
|
[app.common.colors :as clr]
|
|
[app.common.data :as d]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.math :as mth]
|
|
[app.common.pages.helpers :as cph]
|
|
[app.common.types.component :as ctc]
|
|
[app.common.types.modifiers :as ctm]
|
|
[app.common.types.shape-tree :as ctt]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.workspace.changes :as dwc]
|
|
[app.main.data.workspace.colors :as cl]
|
|
[app.main.data.workspace.modifiers :as dwm]
|
|
[app.main.data.workspace.selection :as dwse]
|
|
[app.main.data.workspace.shapes :as dws]
|
|
[app.main.data.workspace.state-helpers :as wsh]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[beicon.core :as rx]
|
|
[potok.core :as ptk]))
|
|
|
|
(def layout-keys
|
|
[:layout
|
|
:layout-flex-dir
|
|
:layout-gap-type
|
|
:layout-gap
|
|
:layout-align-items
|
|
:layout-justify-content
|
|
:layout-align-content
|
|
:layout-wrap-type
|
|
:layout-padding-type
|
|
:layout-padding
|
|
:layout-gap-type])
|
|
|
|
(def initial-flex-layout
|
|
{:layout :flex
|
|
:layout-flex-dir :row
|
|
:layout-gap-type :multiple
|
|
:layout-gap {:row-gap 0 :column-gap 0}
|
|
:layout-align-items :start
|
|
:layout-justify-content :start
|
|
:layout-align-content :stretch
|
|
:layout-wrap-type :nowrap
|
|
:layout-padding-type :simple
|
|
:layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}})
|
|
|
|
(def initial-grid-layout ;; TODO
|
|
{:layout :grid
|
|
:layout-gap-type :multiple
|
|
:layout-gap {:row-gap 0 :column-gap 0}
|
|
:layout-align-items :start
|
|
:layout-align-content :stretch
|
|
:layout-justify-items :start
|
|
:layout-justify-content :start
|
|
:layout-padding-type :simple
|
|
:layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}
|
|
|
|
:layout-grid-rows []
|
|
:layout-grid-columns []})
|
|
|
|
(defn get-layout-initializer
|
|
[type from-frame?]
|
|
(let [initial-layout-data
|
|
(case type
|
|
:flex initial-flex-layout
|
|
:grid initial-grid-layout)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(merge initial-layout-data)
|
|
;; If the original shape is not a frame we set clip content and show-viewer to false
|
|
(cond-> (not from-frame?)
|
|
(assoc :show-content true :hide-in-viewer true))))))
|
|
|
|
|
|
(defn shapes->flex-params
|
|
"Given the shapes calculate its flex parameters (horizontal vs vertical, gaps, etc)"
|
|
([objects shapes]
|
|
(shapes->flex-params objects shapes nil))
|
|
([objects shapes parent]
|
|
(let [points
|
|
(->> shapes
|
|
(map :id)
|
|
(ctt/sort-z-index objects)
|
|
(map (comp gsh/center-shape (d/getf objects))))
|
|
|
|
start (first points)
|
|
end (reduce (fn [acc p] (gpt/add acc (gpt/to-vec start p))) points)
|
|
|
|
angle (gpt/signed-angle-with-other
|
|
(gpt/to-vec start end)
|
|
(gpt/point 1 0))
|
|
|
|
angle (mod angle 360)
|
|
|
|
t1 (min (abs (- angle 0)) (abs (- angle 360)))
|
|
t2 (abs (- angle 90))
|
|
t3 (abs (- angle 180))
|
|
t4 (abs (- angle 270))
|
|
|
|
tmin (min t1 t2 t3 t4)
|
|
|
|
direction
|
|
(cond
|
|
(mth/close? tmin t1) :row
|
|
(mth/close? tmin t2) :column-reverse
|
|
(mth/close? tmin t3) :row-reverse
|
|
(mth/close? tmin t4) :column)
|
|
|
|
selrects (->> shapes
|
|
(mapv :selrect))
|
|
min-x (->> selrects
|
|
(mapv #(min (:x1 %) (:x2 %)))
|
|
(apply min))
|
|
max-x (->> selrects
|
|
(mapv #(max (:x1 %) (:x2 %)))
|
|
(apply max))
|
|
all-width (->> selrects
|
|
(map :width)
|
|
(reduce +))
|
|
column-gap (if (or (= direction :row) (= direction :row-reverse))
|
|
(/ (- (- max-x min-x) all-width) (dec (count shapes)))
|
|
0)
|
|
|
|
min-y (->> selrects
|
|
(mapv #(min (:y1 %) (:y2 %)))
|
|
(apply min))
|
|
max-y (->> selrects
|
|
(mapv #(max (:y1 %) (:y2 %)))
|
|
(apply max))
|
|
all-height (->> selrects
|
|
(map :height)
|
|
(reduce +))
|
|
row-gap (if (or (= direction :column) (= direction :column-reverse))
|
|
(/ (- (- max-y min-y) all-height) (dec (count shapes)))
|
|
0)
|
|
|
|
layout-gap {:row-gap row-gap :column-gap column-gap}
|
|
|
|
parent-selrect (:selrect parent)
|
|
padding (when (and (not (nil? parent)) (> (count shapes) 1))
|
|
{:p1 (min (- min-y (:y1 parent-selrect)) (- (:y2 parent-selrect) max-y))
|
|
:p2 (min (- min-x (:x1 parent-selrect)) (- (:x2 parent-selrect) max-x))})]
|
|
|
|
(cond-> {:layout-flex-dir direction}
|
|
(not (nil? padding)) (assoc :layout-padding {:p1 (:p1 padding) :p2 (:p2 padding) :p3 (:p1 padding) :p4 (:p2 padding)}
|
|
:layout-align-items :center
|
|
:layout-gap layout-gap)))))
|
|
|
|
(defn create-layout-from-id
|
|
[ids type from-frame?]
|
|
(ptk/reify ::create-layout-from-id
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
children-ids (into [] (mapcat #(get-in objects [% :shapes])) ids)
|
|
children-shapes (map (d/getf objects) children-ids)
|
|
parent (get objects (first ids))
|
|
flex-params (when (d/not-empty? children-shapes)
|
|
(shapes->flex-params objects children-shapes parent))
|
|
undo-id (js/Symbol)]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwc/update-shapes ids (get-layout-initializer type from-frame?))
|
|
(dwc/update-shapes
|
|
ids
|
|
(fn [shape]
|
|
(cond-> shape
|
|
(not from-frame?)
|
|
(-> (assoc :layout-item-h-sizing :auto
|
|
:layout-item-v-sizing :auto)
|
|
(merge flex-params)))))
|
|
(ptk/data-event :layout/update ids)
|
|
(dwc/update-shapes children-ids #(dissoc % :constraints-h :constraints-v))
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
|
|
;; Never call this directly but through the data-event `:layout/update`
|
|
;; Otherwise a lot of cycle dependencies could be generated
|
|
(defn- update-layout-positions
|
|
[ids]
|
|
(ptk/reify ::update-layout-positions
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
ids (->> ids (filter #(contains? objects %)))]
|
|
(if (d/not-empty? ids)
|
|
(let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
|
|
(rx/of (dwm/apply-modifiers {:modifiers modif-tree})))
|
|
(rx/empty))))))
|
|
|
|
(defn initialize
|
|
[]
|
|
(ptk/reify ::initialize
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(let [stopper (rx/filter (ptk/type? ::finalize) stream)]
|
|
(->> stream
|
|
(rx/filter (ptk/type? :layout/update))
|
|
(rx/map deref)
|
|
(rx/map #(update-layout-positions %))
|
|
(rx/take-until stopper))))))
|
|
|
|
(defn finalize
|
|
[]
|
|
(ptk/reify ::finalize))
|
|
|
|
|
|
(defn create-layout-from-selection
|
|
[type]
|
|
(ptk/reify ::create-layout-from-selection
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state)
|
|
selected-shapes (map (d/getf objects) selected)
|
|
single? (= (count selected-shapes) 1)
|
|
has-group? (->> selected-shapes (d/seek cph/group-shape?))
|
|
is-group? (and single? has-group?)
|
|
has-mask? (->> selected-shapes (d/seek cph/mask-shape?))
|
|
is-mask? (and single? has-mask?)
|
|
has-component? (some true? (map ctc/instance-root? selected-shapes))
|
|
is-component? (and single? has-component?)]
|
|
|
|
(if (and (not is-component?) is-group? (not is-mask?))
|
|
(let [new-shape-id (uuid/next)
|
|
parent-id (:parent-id (first selected-shapes))
|
|
shapes-ids (:shapes (first selected-shapes))
|
|
ordered-ids (into (d/ordered-set) shapes-ids)
|
|
undo-id (js/Symbol)
|
|
group-index (cph/get-index-replacement selected objects)]
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dwse/select-shapes ordered-ids)
|
|
(dws/create-artboard-from-selection new-shape-id parent-id group-index)
|
|
(cl/remove-all-fills [new-shape-id] {:color clr/black
|
|
:opacity 1})
|
|
(create-layout-from-id [new-shape-id] type false)
|
|
(dwc/update-shapes
|
|
[new-shape-id]
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :layout-item-h-sizing :auto
|
|
:layout-item-v-sizing :auto))))
|
|
;; Set the children to fixed to remove strange interactions
|
|
(dwc/update-shapes
|
|
selected
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :layout-item-h-sizing :fix
|
|
:layout-item-v-sizing :fix))))
|
|
|
|
(ptk/data-event :layout/update [new-shape-id])
|
|
(dws/delete-shapes page-id selected)
|
|
(dwu/commit-undo-transaction undo-id)))
|
|
|
|
(let [new-shape-id (uuid/next)
|
|
undo-id (js/Symbol)
|
|
flex-params (shapes->flex-params objects selected-shapes)]
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dws/create-artboard-from-selection new-shape-id)
|
|
(cl/remove-all-fills [new-shape-id] {:color clr/black
|
|
:opacity 1})
|
|
(create-layout-from-id [new-shape-id] type false)
|
|
(dwc/update-shapes
|
|
[new-shape-id]
|
|
(fn [shape]
|
|
(-> shape
|
|
(merge flex-params)
|
|
(assoc :layout-item-h-sizing :auto
|
|
:layout-item-v-sizing :auto))))
|
|
;; Set the children to fixed to remove strange interactions
|
|
(dwc/update-shapes
|
|
selected
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :layout-item-h-sizing :fix
|
|
:layout-item-v-sizing :fix))))
|
|
|
|
(ptk/data-event :layout/update [new-shape-id])
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn remove-layout
|
|
[ids]
|
|
(ptk/reify ::remove-layout
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [undo-id (js/Symbol)]
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dwc/update-shapes ids #(apply dissoc % layout-keys))
|
|
(ptk/data-event :layout/update ids)
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
(defn create-layout
|
|
[type]
|
|
(ptk/reify ::create-layout
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state)
|
|
selected-shapes (map (d/getf objects) selected)
|
|
single? (= (count selected-shapes) 1)
|
|
is-frame? (= :frame (:type (first selected-shapes)))
|
|
undo-id (js/Symbol)]
|
|
|
|
(if (and single? is-frame?)
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(create-layout-from-id [(first selected)] type true)
|
|
(dwu/commit-undo-transaction undo-id))
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(create-layout-from-selection type)
|
|
(dwu/commit-undo-transaction undo-id)))))))
|
|
|
|
(defn toggle-layout-flex
|
|
[]
|
|
(ptk/reify ::toggle-layout-flex
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (wsh/lookup-page-objects state page-id)
|
|
selected (wsh/lookup-selected state)
|
|
selected-shapes (map (d/getf objects) selected)
|
|
single? (= (count selected-shapes) 1)
|
|
has-flex-layout? (and single? (ctl/flex-layout? objects (:id (first selected-shapes))))]
|
|
|
|
(when (not= 0 (count selected))
|
|
(if has-flex-layout?
|
|
(rx/of (remove-layout selected))
|
|
(rx/of (create-layout :flex))))))))
|
|
|
|
(defn update-layout
|
|
[ids changes]
|
|
(ptk/reify ::update-layout
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [undo-id (js/Symbol)]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwc/update-shapes ids #(d/deep-merge % changes))
|
|
(ptk/data-event :layout/update ids)
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
(defn fix-child-sizing
|
|
[objects parent-changes shape]
|
|
|
|
(let [parent (-> (cph/get-parent objects (:id shape))
|
|
(d/deep-merge parent-changes))
|
|
|
|
auto-width? (ctl/auto-width? parent)
|
|
auto-height? (ctl/auto-height? parent)
|
|
col? (ctl/col? parent)
|
|
row? (ctl/row? parent)
|
|
|
|
all-children (->> parent
|
|
:shapes
|
|
(map (d/getf objects))
|
|
(remove ctl/layout-absolute?))]
|
|
|
|
(cond-> shape
|
|
;; If the parent is hug width and the direction column
|
|
;; change to fixed when ALL children are fill
|
|
(and col? auto-width? (every? ctl/fill-width? all-children))
|
|
(assoc :layout-item-h-sizing :fix)
|
|
|
|
;; If the parent is hug height and the direction is column
|
|
;; change to fixed when ANY children is fill
|
|
(and col? auto-height? (ctl/fill-height? shape))
|
|
(assoc :layout-item-v-sizing :fix)
|
|
|
|
;; If the parent is hug width and the direction row
|
|
;; change to fixed when ANY children is fill
|
|
(and row? auto-width? (ctl/fill-width? shape))
|
|
(assoc :layout-item-h-sizing :fix)
|
|
|
|
;; If the parent is hug height and the direction row
|
|
;; change to fixed when ALL children are fill
|
|
(and row? auto-height? (every? ctl/fill-height? all-children))
|
|
(assoc :layout-item-v-sizing :fix))))
|
|
|
|
(defn fix-parent-sizing
|
|
[objects ids-set changes parent]
|
|
|
|
(let [auto-width? (ctl/auto-width? parent)
|
|
auto-height? (ctl/auto-height? parent)
|
|
col? (ctl/col? parent)
|
|
row? (ctl/row? parent)
|
|
|
|
all-children
|
|
(->> parent :shapes
|
|
(map (d/getf objects))
|
|
(map (fn [shape]
|
|
(if (contains? ids-set (:id shape))
|
|
(d/deep-merge shape changes)
|
|
shape))))]
|
|
|
|
(cond-> parent
|
|
;; Col layout and parent is hug-width if all children are fill-width
|
|
;; change parent to fixed
|
|
(and col? auto-width? (every? ctl/fill-width? all-children))
|
|
(assoc :layout-item-h-sizing :fix)
|
|
|
|
;; Col layout and parent is hug-height if any children is fill-height
|
|
;; change parent to fixed
|
|
(and col? auto-height? (some ctl/fill-height? all-children))
|
|
(assoc :layout-item-v-sizing :fix)
|
|
|
|
;; Row layout and parent is hug-width if any children is fill-width
|
|
;; change parent to fixed
|
|
(and row? auto-width? (some ctl/fill-width? all-children))
|
|
(assoc :layout-item-h-sizing :fix)
|
|
|
|
;; Row layout and parent is hug-height if all children are fill-height
|
|
;; change parent to fixed
|
|
(and row? auto-height? (every? ctl/fill-height? all-children))
|
|
(assoc :layout-item-v-sizing :fix))))
|
|
|
|
(defn update-layout-child
|
|
[ids changes]
|
|
(ptk/reify ::update-layout-child
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (wsh/lookup-page-objects state)
|
|
children-ids (->> ids (mapcat #(cph/get-children-ids objects %)))
|
|
parent-ids (->> ids (map #(cph/get-parent-id objects %)))
|
|
undo-id (js/Symbol)]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwc/update-shapes ids #(d/deep-merge (or % {}) changes))
|
|
(dwc/update-shapes children-ids (partial fix-child-sizing objects changes))
|
|
(dwc/update-shapes parent-ids (partial fix-parent-sizing objects (set ids) changes))
|
|
(ptk/data-event :layout/update ids)
|
|
(dwu/commit-undo-transaction undo-id))))))
|