0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-25 00:06:09 -05:00

Multispan cells auto sizing

This commit is contained in:
alonso.torres 2023-05-03 15:27:40 +02:00
parent 0eff2e8887
commit cdebf245e3
6 changed files with 316 additions and 76 deletions

View file

@ -51,6 +51,7 @@
(ns app.common.geom.shapes.grid-layout.layout-data
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.points :as gpo]
[app.common.types.shape.layout :as ctl]))
@ -134,6 +135,137 @@
(= :auto type)
(assoc :size (min (+ size add-size) max-size)))))))
(defn size-to-allocate
[type parent [child-bounds _] cell]
(let [[row-gap column-gap] (ctl/gaps parent)
[sfn gap prop-span]
(if (= type :column)
[gpo/width-points column-gap :column-span ]
[gpo/height-points row-gap :row-span ])
span (get cell prop-span)]
(- (sfn child-bounds) (* gap (dec span)))))
(defn allocate-size
[allocations indexed-tracks to-allocate]
(if (empty? indexed-tracks)
allocations
(let [[idx track] (first indexed-tracks)
old-allocated (get allocations idx 0.01)
auto-track? (= :auto (:type track))
allocated (if auto-track?
(max old-allocated
(/ to-allocate (count indexed-tracks))
(:size track))
(:size track))]
(recur (cond-> allocations auto-track?
(assoc idx allocated))
(rest indexed-tracks) (- to-allocate allocated)))))
(defn allocate-flex
[allocations indexed-tracks to-allocate fr-value]
(if (empty? indexed-tracks)
allocations
(let [[idx track] (first indexed-tracks)
old-allocated (get allocations idx 0.01)
auto-track? (= :auto (:type track))
flex-track? (= :flex (:type track))
fr (if flex-track? (:value track) 0)
target-allocation (* fr-value fr)
allocated (if (or auto-track? flex-track?)
(max target-allocation
old-allocated
(:size track))
(:size track))]
(recur (cond-> allocations (or flex-track? auto-track?)
(assoc idx allocated))
(rest indexed-tracks)
(- to-allocate allocated)
fr-value))))
(defn set-auto-multi-span
[parent track-list children-map shape-cells type]
(let [[prop prop-span]
(if (= type :column)
[:column :column-span]
[:row :row-span])
;; First calculate allocation without applying so we can modify them on the following tracks
allocate-auto-tracks
(->> shape-cells
(vals)
(filter #(> (get % prop-span) 1))
(sort-by prop-span -)
(reduce
(fn [alloc cell]
(let [shape-id (first (:shapes cell))
from-idx (dec (get cell prop))
to-idx (+ (dec (get cell prop)) (get cell prop-span))
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
has-flex? (->> indexed-tracks (d/seek #(= :flex (:type (second %)))) some?)
to-allocate (size-to-allocate type parent (get children-map shape-id) cell)]
(cond-> alloc
;; skip cells with flex tracks
(and (not has-flex?) (some? to-allocate))
(allocate-size indexed-tracks to-allocate))))
{}))
;; Apply the allocations to the tracks
track-list
(into []
(map-indexed #(update %2 :size max (get allocate-auto-tracks %1)))
track-list)]
track-list))
(defn set-flex-multi-span
[parent track-list children-map shape-cells type]
(let [[prop prop-span]
(if (= type :column)
[:column :column-span]
[:row :row-span])
;; First calculate allocation without applying so we can modify them on the following tracks
allocate-fr-tracks
(->> shape-cells
(vals)
(filter #(> (get % prop-span) 1))
(sort-by prop-span -)
(reduce
(fn [alloc cell]
(let [shape-id (first (:shapes cell))
from-idx (dec (get cell prop))
to-idx (+ (dec (get cell prop)) (get cell prop-span))
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
has-flex? (->> indexed-tracks (d/seek #(= :auto (:type (second %)))) some?)
total-frs (->> indexed-tracks (reduce (fn [tot [_ {:keys [type value]}]]
(cond-> tot
(= type :flex)
(+ value)))
0))
to-allocate (size-to-allocate type parent (get children-map shape-id) cell)
fr-value (when (some? to-allocate) (/ to-allocate total-frs))]
(cond-> alloc
;; skip cells with no flex tracks
(and has-flex? (some? to-allocate))
(allocate-flex indexed-tracks to-allocate fr-value))))
{}))
;; Apply the allocations to the tracks
track-list
(into []
(map-indexed #(update %2 :size max (get allocate-fr-tracks %1)))
track-list)]
track-list))
(defn calc-layout-data
[parent children transformed-parent-bounds]
@ -155,6 +287,10 @@
(->> (:shapes cell) (map #(vector % cell)))))
(:layout-grid-cells parent))
children-map
(into {}
(map #(vector (:id (second %)) %))
children)
;; Initialize tracks
column-tracks
@ -171,8 +307,8 @@
;; Adjust multi-spaned cells with no flex columns
;; TODO
column-tracks (set-auto-multi-span parent column-tracks children-map shape-cells :column)
row-tracks (set-auto-multi-span parent row-tracks children-map shape-cells :row)
;; Calculate the `fr` unit and adjust the size
column-total-size (tracks-total-size column-tracks)
@ -184,6 +320,12 @@
column-frs (tracks-total-frs column-tracks)
row-frs (tracks-total-frs row-tracks)
;; Assign minimum size to the multi-span flex tracks. We do this after calculating
;; the fr size because will affect only the minimum. The maximum will be set by the
;; fracion
column-tracks (set-flex-multi-span parent column-tracks children-map shape-cells :column)
row-tracks (set-flex-multi-span parent row-tracks children-map shape-cells :row)
;; Once auto sizes have been calculated we get calculate the `fr` unit with the remainining size and adjust the size
free-column-space (- bound-width (+ column-total-size column-total-gap))
free-row-space (- bound-height (+ row-total-size row-total-gap))
@ -235,8 +377,7 @@
[(conj tracks (assoc track :start-p start-p))
(gpt/add start-p (vv (+ size row-gap)))])
[[] start-p])
(first))
]
(first))]
{:origin start-p
:layout-bounds layout-bounds
@ -270,4 +411,3 @@
(gpt/to-vec origin row-start-p)))]
(assoc grid-cell :start-p start-p)))))

View file

@ -55,11 +55,13 @@
(defn width-points
[[p0 p1 _ _]]
(max 0.01 (gpt/length (gpt/to-vec p0 p1))))
(when (and (some? p0) (some? p1))
(max 0.01 (gpt/length (gpt/to-vec p0 p1)))))
(defn height-points
[[p0 _ _ p3]]
(max 0.01 (gpt/length (gpt/to-vec p0 p3))))
(when (and (some? p0) (some? p3))
(max 0.01 (gpt/length (gpt/to-vec p0 p3)))))
(defn pad-points
[[p0 p1 p2 p3 :as points] pad-top pad-right pad-bottom pad-left]

View file

@ -262,7 +262,7 @@
(hooks/setup-shortcuts node-editing? drawing-path? text-editing?)
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
[:div.viewport
[:div.viewport {:style #js {"--zoom" zoom}}
[:div.viewport-overlays
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
;; inside a foreign object "dummy" so this awkward behaviour is take into account

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.viewport.grid-layout-editor
(:require-macros [app.main.style :refer [css]])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
@ -20,6 +21,8 @@
[app.main.store :as st]
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -41,36 +44,38 @@
value (unchecked-get props "value")
zoom (unchecked-get props "zoom")
marker-width (/ 24 zoom)
marker-h1 (/ 22 zoom)
marker-h2 (/ 8 zoom)
marker-half-width (/ marker-width 2)
marker-half-height (/ (+ marker-h1 marker-h2) 2)
marker-points
(reduce
apply-to-point
[(gpt/subtract center
(gpt/point (/ 13 zoom) (/ 16 zoom)))]
[#(gpt/add % (gpt/point (/ 26 zoom) 0))
#(gpt/add % (gpt/point 0 (/ 24 zoom)))
#(gpt/add % (gpt/point (- (/ 13 zoom)) (/ 8 zoom)))
#(gpt/subtract % (gpt/point (/ 13 zoom) (/ 8 zoom)))])
(gpt/point marker-half-width marker-half-height))]
[#(gpt/add % (gpt/point marker-width 0))
#(gpt/add % (gpt/point 0 marker-h1))
#(gpt/add % (gpt/point (- marker-half-width) marker-h2))
#(gpt/subtract % (gpt/point marker-half-width marker-h2))])
text-x (:x center)
text-y (:y center)]
[:g.grid-track-marker
[:polygon {:points (->> marker-points
[:g {:class (css :grid-track-marker)}
[:polygon {:class (css :marker-shape)
:points (->> marker-points
(map #(dm/fmt "%,%" (:x %) (:y %)))
(str/join " "))
:style {:fill "var(--color-distance)"
:fill-opacity 0.3}}]
[:text {:x text-x
(str/join " "))}]
[:text {:class (css :marker-text)
:x text-x
:y text-y
:width (/ 26.26 zoom)
:height (/ 32 zoom)
:font-size (/ 16 zoom)
:font-family "worksans"
:font-weight 600
:text-anchor "middle"
:dominant-baseline "middle"
:style {:fill "var(--color-distance)"}}
:dominant-baseline "middle"}
(dm/str value)]]))
(mf/defc grid-editor-frame
@ -95,11 +100,11 @@
#(gpt/add % (vv (+ height (/ 40 zoom))))
#(gpt/add % (hv (/ 40 zoom)))])]
[:polygon {:points (->> frame-points
(map #(dm/fmt "%,%" (:x %) (:y %)))
(str/join " "))
:style {:stroke "var(--color-distance)"
:stroke-width (/ 1 zoom)}}]))
[:polygon
{:class (css :grid-frame)
:points (->> frame-points
(map #(dm/fmt "%,%" (:x %) (:y %)))
(str/join " "))}]))
(mf/defc plus-btn
{::mf/wrap-props false}
@ -127,22 +132,21 @@
(mf/deps on-click)
#(when on-click (on-click)))]
[:g.plus-button {:cursor "pointer"
:on-click handle-click}
[:rect {:x rect-x
[:g {:class (css :grid-plus-button)
:on-click handle-click}
[:rect {:class (css :grid-plus-shape)
:x rect-x
:y rect-y
:width (/ 40 zoom)
:height (/ 40 zoom)
:style {:fill "var(--color-distance)"
:stroke "var(--color-distance)"
:stroke-width (/ 1 zoom)}}]
:height (/ 40 zoom)}]
[:use {:x icon-x
[:use {:class (css :grid-plus-icon)
:x icon-x
:y icon-y
:width (/ 16 zoom)
:height (/ 16 zoom)
:href (dm/str "#icon-plus")
:fill "white"}]]))
:href (dm/str "#icon-plus")}]]))
(defn use-drag
[{:keys [on-drag-start on-drag-end on-drag-delta on-drag-position]}]
@ -259,7 +263,6 @@
:height height
:width width
:style {:fill "transparent" :stroke-width 0 :cursor cursor}
:on-pointer-down handle-pointer-down
:on-lost-pointer-capture handle-lost-pointer-capture
:on-pointer-move handle-pointer-move}]))
@ -301,21 +304,17 @@
[:g.cell-editor
[:rect
{:x (:x start-p)
{:class (dom/classnames (css :grid-cell-outline) true
(css :hover) hover?
(css :selected) selected?)
:x (:x start-p)
:y (:y start-p)
:width cell-width
:height cell-height
:on-pointer-enter #(st/emit! (dwge/hover-grid-cell (:id shape) (:id cell) true))
:on-pointer-leave #(st/emit! (dwge/hover-grid-cell (:id shape) (:id cell) false))
:on-click #(st/emit! (dwge/select-grid-cell (:id shape) (:id cell)))
:style {:fill "transparent"
:stroke "var(--color-distance)"
:stroke-dasharray (when-not (or hover? selected?)
(str/join " " (map #(/ % zoom) [0 8]) ))
:stroke-linecap "round"
:stroke-width (/ 2 zoom)}}]
:on-click #(st/emit! (dwge/select-grid-cell (:id shape) (:id cell)))}]
(when selected?
(let [handlers
@ -451,7 +450,44 @@
(mf/use-callback
(mf/deps (:id shape))
(fn []
(st/emit! (st/emit! (dwsl/add-layout-track [(:id shape)] :row ctl/default-track-value)))))]
(st/emit! (st/emit! (dwsl/add-layout-track [(:id shape)] :row ctl/default-track-value)))))
handle-blur-track-input
(mf/use-callback
(mf/deps (:id shape))
(fn [track-type index event]
(let [target (-> event dom/get-target)
value (-> target dom/get-input-value str/upper)
value-int (d/parse-integer value)
[type value]
(cond
(str/ends-with? value "%")
[:percent value-int]
(str/ends-with? value "FR")
[:flex value-int]
(some? value-int)
[:fixed value-int]
(or (= value "AUTO") (= "" value))
[:auto nil])]
(if (some? type)
(do (obj/set! target "value" (format-size {:type type :value value}))
(dom/set-attribute! target "data-default-value" (format-size {:type type :value value}))
(st/emit! (dwsl/change-layout-track [(:id shape)] track-type index {:type type :value value})))
(obj/set! target "value" (dom/get-attribute target "data-default-value"))))))
handle-keydown-track-input
(mf/use-callback
(fn [event]
(let [enter? (kbd/enter? event)
esc? (kbd/esc? event)]
(when enter?
(dom/blur! (dom/get-target event)))
(when esc?
(dom/blur! (dom/get-target event))))))]
(mf/use-effect
(fn []
@ -482,23 +518,19 @@
(gpt/subtract (hv (/ layout-gap-col 2)))))
text-p (-> start-p
(gpt/subtract (vv (/ 20 zoom)))
(gpt/add (hv (/ (:size column-data) 2))))]
(gpt/subtract (vv (/ 36 zoom))))]
[:* {:key (dm/str "column-" idx)}
[:& track-marker {:center marker-p
:value (dm/str (inc idx))
:zoom zoom}]
[:text {:x (:x text-p)
:y (:y text-p)
:font-size (/ 14 zoom)
:font-weight 600
:font-family "worksans"
:dominant-baseline "central"
:text-anchor "middle"
:style {:fill "var(--color-distance)"}}
(format-size column-data)]
[:foreignObject {:x (:x text-p) :y (:y text-p) :width (max 0 (- (:size column-data) 4)) :height (/ 32 zoom)}
[:input
{:class (css :grid-editor-label)
:type "text"
:default-value (format-size column-data)
:data-default-value (format-size column-data)
:on-key-down handle-keydown-track-input
:on-blur #(handle-blur-track-input :column idx %)}]]
(when (not= idx 0)
[:& resize-handler {:shape shape
:layout-data layout-data
@ -514,24 +546,25 @@
(gpt/subtract (vv (/ layout-gap-row 2)))))
text-p (-> start-p
(gpt/subtract (hv (/ 20 zoom)))
(gpt/add (vv (/ (:size row-data) 2))))]
(gpt/subtract (hv (/ (:size row-data) 2)))
(gpt/subtract (hv (/ 16 zoom)))
(gpt/add (vv (/ (:size row-data) 2)))
(gpt/subtract (vv (/ 18 zoom))))]
[:* {:key (dm/str "row-" idx)}
[:g {:transform (dm/fmt "rotate(-90 % %)" (:x marker-p) (:y marker-p))}
[:& track-marker {:center marker-p
:value (dm/str (inc idx))
:zoom zoom}]]
[:g {:transform (dm/fmt "rotate(-90 % %)" (:x text-p) (:y text-p))}
[:text {:x (:x text-p)
:y (:y text-p)
:font-size (/ 14 zoom)
:font-weight 600
:font-family "worksans"
:dominant-baseline "central"
:text-anchor "middle"
:style {:fill "var(--color-distance)"}}
(format-size row-data)]]
[:g {:transform (dm/fmt "rotate(-90 % %)" (+ (:x text-p) (/ (:size row-data) 2)) (+ (:y text-p) (/ 36 zoom 2)))}
[:foreignObject {:x (:x text-p) :y (:y text-p) :width (:size row-data) :height (/ 36 zoom)}
[:input
{:class (css :grid-editor-label)
:type "text"
:default-value (format-size row-data)
:data-default-value (format-size row-data)
:on-key-down handle-keydown-track-input
:on-blur #(handle-blur-track-input :row idx %)}]]]
(when (not= idx 0)
[:& resize-handler {:shape shape

View file

@ -0,0 +1 @@
{"grid-track-marker":"viewport_grid_layout_editor_grid-track-marker_HABEp","marker-shape":"viewport_grid_layout_editor_marker-shape_FZTUQ","marker-text":"viewport_grid_layout_editor_marker-text_5xM8J","grid-editor-label":"viewport_grid_layout_editor_grid-editor-label_2NbYe","grid-frame":"viewport_grid_layout_editor_grid-frame_CzMnU","grid-plus-button":"viewport_grid_layout_editor_grid-plus-button_brOge","grid-plus-shape":"viewport_grid_layout_editor_grid-plus-shape_jtOU9","grid-plus-icon":"viewport_grid_layout_editor_grid-plus-icon_Zolso","grid-cell-outline":"viewport_grid_layout_editor_grid-cell-outline_1-cRq","hover":"viewport_grid_layout_editor_hover_Rn-tv","selected":"viewport_grid_layout_editor_selected_nhyhL"}

View file

@ -0,0 +1,64 @@
.grid-track-marker {
.marker-shape {
fill: var(--color-distance);
fill-opacity: 0.3;
}
.marker-text {
fill: var(--color-distance);
font-size: calc(12px / var(--zoom));
font-family: worksans;
font-weight: 600;
}
}
.grid-editor-label {
background: none;
width: 100%;
height: 100%;
text-align: center;
font-family: worksans;
color: var(--color-distance);
font-weight: 600;
margin: 0;
padding: 0;
border: 0;
font-size: calc(12px / var(--zoom));
&:focus {
outline: none;
border-bottom: calc(1px / var(--zoom)) solid var(--color-distance);
}
}
.grid-frame {
fill: transparent;
stroke: var(--color-distance);
stroke-width: calc(1 / var(--zoom));
}
.grid-plus-button {
cursor: pointer;
.grid-plus-shape {
fill: var(--color-distance);
stroke: var(--color-distance);
stroke-width: calc(1 / var(--zoom));
}
.grid-plus-icon {
fill: white;
}
}
.grid-cell-outline {
fill: transparent;
stroke: var(--color-distance);
stroke-linecap: round;
stroke-width: calc(2 / var(--zoom));
stroke-dasharray: 0 calc(8 / var(--zoom));
&.hover,
&.selected {
stroke-dasharray: initial;
}
}