diff --git a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc index 79de30499..b89b372b4 100644 --- a/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc +++ b/common/src/app/common/geom/shapes/grid_layout/layout_data.cljc @@ -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))))) - diff --git a/common/src/app/common/geom/shapes/points.cljc b/common/src/app/common/geom/shapes/points.cljc index 3722beb9e..ff6bd15df 100644 --- a/common/src/app/common/geom/shapes/points.cljc +++ b/common/src/app/common/geom/shapes/points.cljc @@ -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] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 48ebc40b7..8af5e3702 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs index dee2f761c..f9a6b1313 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.css.json b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.css.json new file mode 100644 index 000000000..2ab84c5e5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.css.json @@ -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"} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss new file mode 100644 index 000000000..8a8a13659 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -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; + } +}