diff --git a/CHANGES.md b/CHANGES.md index b6a236f67..97c409534 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ ### :sparkles: New features +- Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394) - Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283) - Rotation to snap to 15ยบ intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437) - Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497) diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index 05332c5be..febb78446 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -36,6 +36,7 @@ ;; :rx nil ;; :ry nil} ;; + (defn get-attrs-multi ([objs attrs] (get-attrs-multi objs attrs = identity)) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 1d0e3e6a1..bcab57685 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -9,12 +9,13 @@ [app.common.colors :as clr] [app.common.uuid :as uuid])) -(def file-version 13) +(def file-version 14) (def default-color clr/gray-20) (def root uuid/zero) (def component-sync-attrs {:name :name-group + :fills :fill-group :fill-color :fill-group :fill-opacity :fill-group :fill-color-gradient :fill-group diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc index 21a209c35..536fc5172 100644 --- a/common/src/app/common/pages/init.cljc +++ b/common/src/app/common/pages/init.cljc @@ -33,16 +33,16 @@ (def default-frame-attrs {:frame-id uuid/zero - :fill-color clr/white - :fill-opacity 1 + :fills [{:fill-color clr/white + :fill-opacity 1}] :shapes [] :hide-fill-on-export false}) (def ^:private minimal-shapes [{:type :rect :name "Rect-1" - :fill-color default-color - :fill-opacity 1 + :fills [{:fill-color default-color + :fill-opacity 1}] :stroke-style :none :stroke-alignment :center :stroke-width 0 @@ -53,12 +53,13 @@ {:type :image :rx 0 - :ry 0} + :ry 0 + :fills []} {:type :circle :name "Circle-1" - :fill-color default-color - :fill-opacity 1 + :fills [{:fill-color default-color + :fill-opacity 1}] :stroke-style :none :stroke-alignment :center :stroke-width 0 @@ -67,6 +68,7 @@ {:type :path :name "Path-1" + :fills [] :stroke-style :solid :stroke-alignment :center :stroke-width 2 @@ -75,8 +77,8 @@ {:type :frame :name "Artboard-1" - :fill-color clr/white - :fill-opacity 1 + :fills [{:fill-color clr/white + :fill-opacity 1}] :stroke-style :none :stroke-alignment :center :stroke-width 0 diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index d87f2156e..464df6630 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -300,3 +300,33 @@ (update page :objects #(d/mapm update-object %)))] (update data :pages-index #(d/mapm update-page %)))) + +(defn set-fills + [shape] + (let [attrs {:fill-color (:fill-color shape) + :fill-color-gradient (:fill-color-gradient shape) + :fill-color-ref-file (:fill-color-ref-file shape) + :fill-color-ref-id (:fill-color-ref-id shape) + :fill-opacity (:fill-opacity shape)} + + clean-attrs (d/without-nils attrs)] + (-> shape + (assoc :fills [clean-attrs]) + (dissoc :fill-color) + (dissoc :fill-color-gradient) + (dissoc :fill-color-ref-file) + (dissoc :fill-color-ref-id) + (dissoc :fill-opacity)))) + +;; Add fills to shapes +(defmethod migrate 14 + [data] + (letfn [(update-object [_ object] + (cond-> object + (and (not (= :text (:type object))) (nil? (:fills object))) + (set-fills))) + + (update-page [_ page] + (update page :objects #(d/mapm update-object %)))] + + (update data :pages-index #(d/mapm update-page %)))) diff --git a/frontend/resources/styles/main/partials/colorpicker.scss b/frontend/resources/styles/main/partials/colorpicker.scss index dbd3fff3d..691f06da6 100644 --- a/frontend/resources/styles/main/partials/colorpicker.scss +++ b/frontend/resources/styles/main/partials/colorpicker.scss @@ -499,6 +499,10 @@ margin-bottom: $size-2; position: relative; + &[draggable="true"] { + cursor: pointer; + } + .color-name { font-size: $fs12; margin: 5px 6px 0px 6px; diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index e9bd57dab..f46983802 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -954,6 +954,7 @@ justify-content: center; align-items: center; cursor: pointer; + svg { width: 12px; height: 12px; @@ -961,6 +962,11 @@ stroke: $color-gray-20; } + &.remove { + min-width: 20px; + min-height: 20px; + } + &:hover svg, &.active svg { fill: $color-primary; diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 0cb4eca12..4ee3fee66 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -111,42 +111,105 @@ (assoc-in [:workspace-local :picked-color-select] value) (assoc-in [:workspace-local :picked-shift?] shift?))))) +(defn transform-fill + [state ids color transform] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + + is-text? #(= :text (:type (get objects %))) + text-ids (filter is-text? ids) + shape-ids (filter (comp not is-text?) ids) + + attrs (cond-> {:fill-color nil + :fill-color-gradient nil + :fill-color-ref-file nil + :fill-color-ref-id nil + :fill-opacity nil} + + (contains? color :color) + (assoc :fill-color (:color color)) + + (contains? color :id) + (assoc :fill-color-ref-id (:id color)) + + (contains? color :file-id) + (assoc :fill-color-ref-file (:file-id color)) + + (contains? color :gradient) + (assoc :fill-color-gradient (:gradient color)) + + (contains? color :opacity) + (assoc :fill-opacity (:opacity color))) + ;; Not nil attrs + clean-attrs (d/without-nils attrs)] + + (rx/concat + (rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids)) + (rx/of (dch/update-shapes + shape-ids + #(transform % clean-attrs)))))) + +(defn swap-fills [shape index new-index] + (let [first (get-in shape [:fills index]) + second (get-in shape [:fills new-index])] + (-> shape + (assoc-in [:fills index] second) + (assoc-in [:fills new-index] first)) + )) + +(defn reorder-fills + [ids index new-index] + (ptk/reify ::reorder-fills + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes + ids + #(swap-fills % index new-index)))))) + (defn change-fill - [ids color] + [ids color position] (ptk/reify ::change-fill ptk/WatchEvent (watch [_ state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [change (fn [shape attrs] (assoc-in shape [:fills position] (into {} attrs)))] + (transform-fill state ids color change))))) - is-text? #(= :text (:type (get objects %))) - text-ids (filter is-text? ids) - shape-ids (filter (comp not is-text?) ids) +(defn change-fill-and-clear + [ids color] + (ptk/reify ::change-fill-and-clear + ptk/WatchEvent + (watch [_ state _] + (let [set (fn [shape attrs] (assoc shape :fills [attrs]))] + (transform-fill state ids color set))))) - attrs (cond-> {:fill-color nil - :fill-color-gradient nil - ::fill-color-ref-file nil - :fill-color-ref-id nil - :fill-opacity nil} +(defn add-fill + [ids color] + (ptk/reify ::add-fill + ptk/WatchEvent + (watch [_ state _] + (let [add (fn [shape attrs] (assoc shape :fills (into [attrs] (:fills shape))))] + (transform-fill state ids color add))))) - (contains? color :color) - (assoc :fill-color (:color color)) +(defn remove-fill + [ids color position] + (ptk/reify ::remove-fill + ptk/WatchEvent + (watch [_ state _] + (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values) + (filterv (fn [[idx _]] (not= idx index))) + (mapv second))) - (contains? color :id) - (assoc :fill-color-ref-id (:id color)) + remove (fn [shape _] (update shape :fills remove-fill-by-index position))] + (transform-fill state ids color remove))))) - (contains? color :file-id) - (assoc :fill-color-ref-file (:file-id color)) +(defn remove-all-fills + [ids color] + (ptk/reify ::remove-all-fills + ptk/WatchEvent + (watch [_ state _] + (let [remove-all (fn [shape _] (assoc shape :fills []))] + (transform-fill state ids color remove-all))))) - (contains? color :gradient) - (assoc :fill-color-gradient (:gradient color)) - - (contains? color :opacity) - (assoc :fill-opacity (:opacity color)))] - - (rx/concat - (rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids)) - (rx/of (dch/update-shapes shape-ids (fn [shape] (d/merge shape attrs))))))))) (defn change-hide-fill-on-export [ids hide-fill-on-export] @@ -207,7 +270,7 @@ update-events (fn [color] - (rx/of (change-fill ids color)))] + (rx/of (change-fill ids color 0)))] (rx/merge ;; Stream that updates the stroke/width and stops if `esc` pressed diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index b649250aa..cd456cce6 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -73,26 +73,28 @@ (defn setup-fill [shape] (cond-> shape ;; Color present as attribute - (uc/color? (get-in shape [:svg-attrs :fill])) + (uc/color? (str/trim (get-in shape [:svg-attrs :fill]))) (-> (update :svg-attrs dissoc :fill) - (assoc :fill-color (-> (get-in shape [:svg-attrs :fill]) - (uc/parse-color)))) + (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :fill]) + (str/trim) + (uc/parse-color)))) ;; Color present as style - (uc/color? (get-in shape [:svg-attrs :style :fill])) + (uc/color? (str/trim (get-in shape [:svg-attrs :style :fill]))) (-> (update-in [:svg-attrs :style] dissoc :fill) - (assoc :fill-color (-> (get-in shape [:svg-attrs :style :fill]) - (uc/parse-color)))) + (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :style :fill]) + (str/trim) + (uc/parse-color)))) (get-in shape [:svg-attrs :fill-opacity]) (-> (update :svg-attrs dissoc :fill-opacity) - (assoc :fill-opacity (-> (get-in shape [:svg-attrs :fill-opacity]) - (d/parse-double)))) + (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity]) + (d/parse-double)))) (get-in shape [:svg-attrs :style :fill-opacity]) (-> (update-in [:svg-attrs :style] dissoc :fill-opacity) - (assoc :fill-opacity (-> (get-in shape [:svg-attrs :style :fill-opacity]) - (d/parse-double)))))) + (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity]) + (d/parse-double)))))) (defn setup-stroke [shape] (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap]) @@ -378,9 +380,10 @@ :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) :image (create-image-shape name frame-id svg-data element-data) - #_other (create-raw-svg name frame-id svg-data element-data)) + #_other (create-raw-svg name frame-id svg-data element-data))) + + shape (assoc shape :fills []) - ) shape (when (some? shape) (-> shape (assoc :svg-defs (select-keys (:defs svg-data) references)) diff --git a/frontend/src/app/main/ui/components/color_input.cljs b/frontend/src/app/main/ui/components/color_input.cljs index da2cc7fb2..a14c84418 100644 --- a/frontend/src/app/main/ui/components/color_input.cljs +++ b/frontend/src/app/main/ui/components/color_input.cljs @@ -29,6 +29,7 @@ [props external-ref] (let [value (obj/get props "value") on-change (obj/get props "onChange") + on-blur (obj/get props "onBlur") ;; We need a ref pointing to the input dom element, but the user ;; of this component may provide one (that is forwarded here). @@ -92,6 +93,8 @@ (mf/deps parse-value apply-value update-input) (fn [_] (let [new-value (parse-value)] + (when on-blur + (on-blur)) (if new-value (apply-value new-value) (update-input value))))) diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs index 6d021f534..0db2dfb00 100644 --- a/frontend/src/app/main/ui/render.cljs +++ b/frontend/src/app/main/ui/render.cljs @@ -70,7 +70,7 @@ object (cond-> object (:hide-fill-on-export object) - (assoc :fill-color nil :fill-opacity 0)) + (assoc :fills [])) {:keys [x y width height] :as bs} (calc-bounds object objects) [_ _ width height :as coords] (->> [x y width height] (map #(* % zoom))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index dd6a041ab..d85ebc5f5 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -79,14 +79,15 @@ "z")})) attrs)) -(defn add-fill [attrs shape render-id] - (let [fill-attrs (cond +(defn add-fill [attrs shape render-id index] + (let [ + fill-attrs (cond (contains? shape :fill-image) (let [fill-image-id (str "fill-image-" render-id)] {:fill (str/format "url(#%s)" fill-image-id)}) (contains? shape :fill-color-gradient) - (let [fill-color-gradient-id (str "fill-color-gradient_" render-id)] + (let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)] {:fill (str/format "url(#%s)" fill-color-gradient-id)}) (contains? shape :fill-color) @@ -193,9 +194,23 @@ styles (-> (obj/get props "style" (obj/new)) (obj/merge! svg-styles) - (add-fill shape render-id) (add-stroke shape render-id) - (add-layer-props shape))] + (add-layer-props shape)) + + styles (cond (or (some? (:fill-image shape)) + (= :image (:type shape)) + (> (count (:fills shape)) 1) + (some #(some? (:fill-color-gradient %)) (:fills shape))) + (obj/set! styles "fill" (str "url(#fill-" render-id ")")) + + ;; imported svgs can have fill and fill-opacity attributes + (obj/contains? svg-styles "fill") + (-> styles + (obj/set! "fill" (obj/get svg-styles "fill")) + (obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity"))) + + :else + (add-fill styles (get-in shape [:fills 0]) render-id 0))] (-> props (obj/merge! svg-attrs) @@ -208,10 +223,10 @@ (add-style-attrs shape))) (defn extract-fill-attrs - [shape] + [shape index] (let [render-id (mf/use-ctx muc/render-ctx) fill-styles (-> (obj/get shape "style" (obj/new)) - (add-fill shape render-id))] + (add-fill shape render-id index))] (-> (obj/new) (obj/set! "style" fill-styles)))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index d61fe8a49..b062f6ddd 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -103,11 +103,6 @@ (-> (add! :rx) (add! :ry))) - (cond-> image? - (-> (add! :fill-color) - (add! :fill-opacity) - (add! :fill-color-gradient))) - (cond-> path? (-> (add! :stroke-cap-start) (add! :stroke-cap-end))) @@ -293,6 +288,7 @@ :penpot:background-overlay ((d/nilf str) (:background-overlay interaction)) :penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])]))) + (mf/defc export-data [{:keys [shape]}] (let [props (-> (obj/new) (add-data shape) (add-library-refs shape))] diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs deleted file mode 100644 index e3c8f2dc7..000000000 --- a/frontend/src/app/main/ui/shapes/fill_image.cljs +++ /dev/null @@ -1,46 +0,0 @@ -;; 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.ui.shapes.fill-image - (:require - [app.common.geom.shapes :as gsh] - [app.config :as cfg] - [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.embed :as embed] - [app.util.object :as obj] - [rumext.alpha :as mf])) - -(mf/defc fill-image-pattern - {::mf/wrap-props false} - [props] - - (let [shape (obj/get props "shape") - render-id (obj/get props "render-id")] - (when (contains? shape :fill-image) - (let [{:keys [x y width height]} (:selrect shape) - fill-image-id (str "fill-image-" render-id) - uri (cfg/resolve-file-media (:fill-image shape)) - embed (embed/use-data-uris [uri]) - transform (gsh/transform-matrix shape) - shape-without-image (dissoc shape :fill-image) - fill-attrs (-> (attrs/extract-fill-attrs shape-without-image) - (obj/set! "width" width) - (obj/set! "height" height))] - - [:pattern {:id fill-image-id - :patternUnits "userSpaceOnUse" - :x x - :y y - :height height - :width width - :patternTransform transform - :data-loading (str (not (contains? embed uri)))} - [:g - [:> :rect fill-attrs] - [:image {:xlinkHref (get embed uri uri) - :width width - :height height}] - ]])))) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs new file mode 100644 index 000000000..5012c1ae9 --- /dev/null +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -0,0 +1,64 @@ +;; 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.ui.shapes.fills + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.config :as cfg] + [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.embed :as embed] + [app.main.ui.shapes.gradients :as grad] + [app.util.object :as obj] + [rumext.alpha :as mf])) + +(mf/defc fills + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + render-id (obj/get props "render-id") + {:keys [x y width height]} (:selrect shape) + {:keys [metadata]} shape + fill-id (str "fill-" render-id) + has-image (or metadata (:fill-image shape)) + uri (if metadata + (cfg/resolve-file-media metadata) + (cfg/resolve-file-media (:fill-image shape))) + embed (embed/use-data-uris [uri]) + transform (gsh/transform-matrix shape) + pattern-attrs (cond-> #js {:id fill-id + :patternUnits "userSpaceOnUse" + :x x + :y y + :height height + :width width + :data-loading (str (not (contains? embed uri)))} + (= :path (:type shape)) + (obj/set! "patternTransform" transform))] + + [:* + (for [[index value] (-> (d/enumerate (:fills shape [])) reverse)] + (cond (some? (:fill-color-gradient value)) + (case (:type (:fill-color-gradient value)) + :linear [:> grad/linear-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index) + :gradient (:fill-color-gradient value) + :shape shape}] + :radial [:> grad/radial-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index) + :gradient (:fill-color-gradient value) + :shape shape}]))) + + [:> :pattern pattern-attrs + [:g + (for [[index value] (-> (d/enumerate (:fills shape [])) reverse)] + [:> :rect (-> (attrs/extract-fill-attrs value index) + (obj/set! "width" width) + (obj/set! "height" height))]) + + (when has-image + [:image {:xlinkHref (get embed uri uri) + :width width + :height height}])]]])) diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index cdb4aea31..8b6cb854b 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -51,10 +51,6 @@ shape (unchecked-get props "shape") {:keys [x y width height]} shape - has-background? (or (some? (:fill-color shape)) - (some? (:fill-color-gradient shape))) - has-stroke? (not= :none (:stroke-style shape)) - props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:x x @@ -63,8 +59,8 @@ :height height :className "frame-background"}))] [:* - (when (or has-background? has-stroke?) - [:> :rect props]) + [:> :rect props] + (for [item childs] [:& shape-wrapper {:shape item :key (:id item)}])]))) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 988c52247..af784319f 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -7,11 +7,8 @@ (ns app.main.ui.shapes.image (:require [app.common.geom.shapes :as gsh] - [app.config :as cfg] - [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] - [app.main.ui.shapes.embed :as embed] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -20,19 +17,8 @@ [props] (let [shape (unchecked-get props "shape") - {:keys [x y width height metadata]} shape - uri (cfg/resolve-file-media metadata) - embed (embed/use-data-uris [uri]) - + {:keys [x y width height]} shape transform (gsh/transform-matrix shape) - - fill-attrs (-> (attrs/extract-fill-attrs shape) - (obj/set! "width" width) - (obj/set! "height" height)) - - render-id (mf/use-ctx muc/render-ctx) - fill-image-id (str "fill-image-" render-id) - shape (assoc shape :fill-image fill-image-id) props (-> (attrs/extract-style-attrs shape) (obj/merge! (attrs/extract-border-radius-attrs shape)) (obj/merge! @@ -44,19 +30,6 @@ path? (some? (.-d props))] [:g - [:defs - [:pattern {:id fill-image-id - :patternUnits "userSpaceOnUse" - :x x - :y y - :height height - :width width - :data-loading (str (not (contains? embed uri)))} - [:g - [:> :rect fill-attrs] - [:image {:xlinkHref (get embed uri uri) - :width width - :height height}]]]] [:& shape-custom-stroke {:shape shape} (if path? [:> :path props] diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index 36155fc7e..dc658c119 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -13,8 +13,6 @@ [app.util.path.format :as upf] [rumext.alpha :as mf])) -;; --- Path Shape - (mf/defc path-shape {::mf/wrap-props false} [props] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index cfb73aad9..33ee5c8be 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -12,7 +12,7 @@ [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :as cs] [app.main.ui.shapes.export :as ed] - [app.main.ui.shapes.fill-image :as fim] + [app.main.ui.shapes.fills :as fills] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.gradients :as grad] @@ -63,9 +63,12 @@ [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] - [:& grad/gradient {:shape shape :attr :fill-color-gradient}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] - [:& fim/fill-image-pattern {:shape shape :render-id render-id}] + (when (or (some? (:fill-image shape)) + (= :image (:type shape)) + (> (count (:fills shape)) 1) + (some :fill-color-gradient (:fills shape))) + [:& fills/fills {:shape shape :render-id render-id}]) [:& cs/stroke-defs {:shape shape :render-id render-id}] [:& frame/frame-clip-def {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs index 8c600a2d7..f24df7b6e 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/fill.cljs @@ -26,7 +26,8 @@ (and (not (contains? #{:text :group} (:type shape))) (or (:fill-color shape) - (:fill-color-gradient shape)))) + (:fill-color-gradient shape) + (seq (:fills shape))))) (defn copy-data [shape] (cg/generate-css-props @@ -55,5 +56,9 @@ [:& copy-button {:data (copy-data (first shapes))}])] (for [shape shapes] - [:& fill-block {:key (str "fill-block-" (:id shape)) - :shape shape}])]))) + (if (seq (:fills shape)) + (for [value (:fills shape [])] + [:& fill-block {:key (str "fill-block-" (:id shape)) + :shape value}]) + [:& fill-block {:key (str "fill-block-" (:id shape)) + :shape shape}]))]))) diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index 86b79e0f4..4ed80552d 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -46,7 +46,7 @@ (fn [event] (if (kbd/alt? event) (st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color))) - (st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color)))))] + (st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color) 0))))] [:div.color-cell {:on-click select-color} [:& cb/color-bullet {:color color}] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 8ed877406..773d92719 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -799,7 +799,7 @@ (let [ids (wsh/lookup-selected @st/state)] (if (kbd/alt? event) (st/emit! (dc/change-stroke ids color)) - (st/emit! (dc/change-fill ids color))))) + (st/emit! (dc/change-fill ids color 0))))) rename-color (fn [name] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index fd79acb39..ba0595fff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -6,18 +6,23 @@ (ns app.main.ui.workspace.sidebar.options.menus.fill (:require + [app.common.attrs :as attrs] + [app.common.colors :as clr] + [app.common.data :as d] [app.common.pages :as cp] [app.main.data.workspace.colors :as dc] [app.main.store :as st] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] - [app.util.color :as uc] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [cuerdas.core :as str] [rumext.alpha :as mf])) (def fill-attrs - [:fill-color + [:fills + :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file @@ -27,24 +32,60 @@ (def fill-attrs-shape (conj fill-attrs :hide-fill-on-export)) +(defn color-values + [color] + {:color (:fill-color color) + :opacity (:fill-opacity color) + :id (:fill-color-ref-id color) + :file-id (:fill-color-ref-file color) + :gradient (:fill-color-gradient color)}) + (mf/defc fill-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]} [{:keys [ids type values disable-remove?] :as props}] - (let [show? (or (not (nil? (:fill-color values))) - (not (nil? (:fill-color-gradient values)))) - - label (case type + (let [label (case type :multiple (tr "workspace.options.selection-fill") :group (tr "workspace.options.group-fill") (tr "workspace.options.fill")) - color {:color (:fill-color values) - :opacity (:fill-opacity values) - :id (:fill-color-ref-id values) - :file-id (:fill-color-ref-file values) - :gradient (:fill-color-gradient values)} + ;; Excluding nil values + values (d/without-nils values) - hide-fill-on-export? (:hide-fill-on-export values) + only-shapes? (and (contains? values :fills) + ;; texts have :fill-* attributes, the rest of the shapes have :fills + (= (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0)) + + shapes-and-texts? (and (contains? values :fills) + ;; texts have :fill-* attributes, the rest of the shapes have :fills + (> (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0)) + + ;; Texts still have :fill-* attributes and the rest of the shapes just :fills so we need some extra calculation when multiple selection happens to detect them + plain-values (if (vector? (:fills values)) + (concat (:fills values) [(dissoc values :fills)]) + values) + + plain-values (attrs/get-attrs-multi plain-values [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient]) + + plain-values (if (empty? plain-values) + values + plain-values) + + ;; We must control some rare situations like + ;; - Selecting texts and shapes with different fills + ;; - Selecting a text and a shape with empty fills + plain-values (if (and shapes-and-texts? + (or + (= (:fills values) :multiple) + (= 0 (count (:fills values))))) + {:fills :multiple + :fill-color :multiple + :fill-opacity :multiple + :fill-color-ref-id :multiple + :fill-color-ref-file :multiple + :fill-color-gradient :multiple} + plain-values) + + hide-fill-on-export? (:hide-fill-on-export values false) checkbox-ref (mf/use-ref) @@ -52,31 +93,47 @@ (mf/use-callback (mf/deps ids) (fn [_] - (st/emit! (dc/change-fill ids {:color cp/default-color - :opacity 1})))) - - on-delete - (mf/use-callback - (mf/deps ids) - (fn [_] - (st/emit! (dc/change-fill ids (into {} uc/empty-color))))) + (st/emit! (dc/add-fill ids {:color cp/default-color + :opacity 1})))) on-change + (mf/use-callback + (mf/deps ids) + (fn [index] + (fn [color] + (st/emit! (dc/change-fill ids color index))))) + + on-reorder + (mf/use-callback + (mf/deps ids) + (fn [new-index] + (fn [index] + (st/emit! (dc/reorder-fills ids index new-index))))) + + on-change-mixed-shapes (mf/use-callback (mf/deps ids) (fn [color] - (let [remove-multiple (fn [[_ value]] (not= value :multiple)) - color (into {} (filter remove-multiple) color)] - (st/emit! (dc/change-fill ids color))))) + (st/emit! (dc/change-fill-and-clear ids color)))) + + on-remove + (fn [index] + (fn [] + (st/emit! (dc/remove-fill ids {:color cp/default-color + :opacity 1} index)))) + on-remove-all + (fn [_] + (st/emit! (dc/remove-all-fills ids {:color clr/black + :opacity 1}))) on-detach (mf/use-callback (mf/deps ids) - (fn [] - (let [remove-multiple (fn [[_ value]] (not= value :multiple)) - color (-> (into {} (filter remove-multiple) color) - (assoc :id nil :file-id nil))] - (st/emit! (dc/change-fill ids color))))) + (fn [index] + (fn [color] + (let [color (-> color + (assoc :id nil :file-id nil))] + (st/emit! (dc/change-fill ids color index)))))) on-change-show-fill-on-export (mf/use-callback @@ -95,18 +152,46 @@ (dom/set-attribute checkbox "indeterminate" true) (dom/remove-attribute checkbox "indeterminate"))))) - (if show? [:div.element-set [:div.element-set-title [:span label] - (when (not disable-remove?) - [:div.add-page {:on-click on-delete} i/minus])] + (when (and (not disable-remove?) (not (= :multiple (:fills values))) only-shapes?) + [:div.add-page {:on-click on-add} i/close])] [:div.element-set-content - [:& color-row {:color color - :title (tr "workspace.options.fill") - :on-change on-change - :on-detach on-detach}] + + (if only-shapes? + (cond + (= :multiple (:fills values)) + [:div.element-set-options-group + [:div.element-set-label (tr "settings.multiple")] + [:div.element-set-actions + [:div.element-set-actions-button {:on-click on-remove-all} + i/minus]]] + + (seq (:fills values)) + [:& h/sortable-container {} + (for [[index value] (d/enumerate (:fills values []))] + [:& color-row {:color {:color (:fill-color value) + :opacity (:fill-opacity value) + :id (:fill-color-ref-id value) + :file-id (:fill-color-ref-file value) + :gradient (:fill-color-gradient value)} + :index index + :title (tr "workspace.options.fill") + :on-change (on-change index) + :on-reorder (on-reorder index) + :on-detach (on-detach index) + :on-remove (on-remove index)}])]) + + [:& color-row {:color {:color (:fill-color plain-values) + :opacity (:fill-opacity plain-values) + :id (:fill-color-ref-id plain-values) + :file-id (:fill-color-ref-file plain-values) + :gradient (:fill-color-gradient plain-values)} + :title (tr "workspace.options.fill") + :on-change on-change-mixed-shapes + :on-detach (on-detach 0)}]) (when (or (= type :frame) (and (= type :multiple) (some? hide-fill-on-export?))) @@ -118,9 +203,4 @@ :on-change on-change-show-fill-on-export}] [:label {:for "show-fill-on-export"} - (tr "workspace.options.show-fill-on-export")]])]] - - [:div.element-set - [:div.element-set-title - [:span label] - [:div.add-page {:on-click on-add} i/close]]]))) + (tr "workspace.options.show-fill-on-export")]])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index b151b7e15..8ab359d52 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -61,11 +61,12 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close title]}] + [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) hover-detach (mf/use-state false) + disable-drag (mf/use-state false) get-color-name (fn [{:keys [id file-id]}] (let [src-colors (if (= file-id current-file-id) @@ -77,6 +78,9 @@ (-> color (update :color #(or % (:value color))))) + detach-value (fn [] + (when on-detach (on-detach color))) + change-value (fn [new-value] (when on-change (on-change (-> color (assoc :color new-value) @@ -104,7 +108,12 @@ (change-opacity (/ value 100))) select-all (fn [event] - (dom/select-text! (dom/get-target event))) + (when (not @disable-drag) + (dom/select-text! (dom/get-target event))) + (reset! disable-drag true)) + + on-blur (fn [_] + (reset! disable-drag false)) handle-click-color (mf/use-callback (mf/deps color) @@ -115,7 +124,22 @@ handle-open handle-close)) - prev-color (h/use-previous color)] + prev-color (h/use-previous color) + + on-drop + (fn [_ data] + (on-reorder (:index data))) + + [dprops dref] (if (some? on-reorder) + (h/use-sortable + :data-type "penpot/color-row" + :on-drop on-drop + :disabled @disable-drag + :detect-center? false + :data {:id (str "color-row-" index) + :index index + :name (str "Color row" index)}) + [nil nil])] (mf/use-effect (mf/deps color prev-color) @@ -123,7 +147,11 @@ (when (not= prev-color color) (modal/update-props! :colorpicker {:data (parse-color color)})))) - [:div.row-flex.color-data {:title title} + [:div.row-flex.color-data {:title title + :class (dom/classnames + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} [:& cb/color-bullet {:color color :on-click handle-click-color}] @@ -137,7 +165,7 @@ [:div.element-set-actions-button {:on-mouse-enter #(reset! hover-detach true) :on-mouse-leave #(reset! hover-detach false) - :on-click on-detach} + :on-click detach-value} (if @hover-detach i/unchain i/chain)])] ;; Rendering a gradient @@ -156,6 +184,7 @@ (-> color :color uc/remove-hash)) :placeholder (tr "settings.multiple") :on-click select-all + :on-blur on-blur :on-change handle-value-change}]] (when (and (not disable-opacity) @@ -167,5 +196,7 @@ :on-click select-all :on-change handle-opacity-change :min 0 - :max 100}]])])])) + :max 100}]])]) + (when (some? on-remove) + [:div.element-set-actions-button.remove {:on-click on-remove} i/minus])])) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 63cc62668..dac15ef0e 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -358,16 +358,40 @@ (= type :path) (parse-path center svg-data)))) +(defn add-library-refs + [props node] + + (let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid) + stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid) + component-id (get-meta node :component-id uuid/uuid) + component-file (get-meta node :component-file uuid/uuid) + shape-ref (get-meta node :shape-ref uuid/uuid) + component-root? (get-meta node :component-root str->bool)] + + (cond-> props + (some? stroke-color-ref-id) + (assoc :stroke-color-ref-id stroke-color-ref-id + :stroke-color-ref-file stroke-color-ref-file) + + (some? component-id) + (assoc :component-id component-id + :component-file component-file) + + component-root? + (assoc :component-root? component-root?) + + (some? shape-ref) + (assoc :shape-ref shape-ref)))) + (defn add-fill [props node svg-data] (let [fill (:fill svg-data) hide-fill-on-export (get-meta node :hide-fill-on-export str->bool) + fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid) + fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid) gradient (when (str/starts-with? fill "url") - (parse-gradient node fill)) - meta-fill-color (get-meta node :fill-color) - meta-fill-opacity (get-meta node :fill-opacity) - meta-fill-color-gradient (get-meta node :fill-color-gradient)] + (parse-gradient node fill))] (cond-> props :always @@ -386,14 +410,9 @@ (some? hide-fill-on-export) (assoc :hide-fill-on-export hide-fill-on-export) - (some? meta-fill-color) - (assoc :fill-color meta-fill-color - :fill-opacity (d/parse-double meta-fill-opacity)) - - (some? meta-fill-color-gradient) - (assoc :fill-color-gradient meta-fill-color-gradient - :fill-color nil - :fill-opacity nil)))) + (some? fill-color-ref-id) + (assoc :fill-color-ref-id fill-color-ref-id + :fill-color-ref-file fill-color-ref-file)))) (defn add-stroke [props node svg-data] @@ -658,6 +677,21 @@ props))) +(defn add-fills + [props node svg-data] + (let [fills (-> node + (find-node :defs) + (find-node :pattern) + (find-node :g) + (find-all-nodes :rect) + (reverse)) + fills (if (= 0 (count fills)) + [(add-fill {} node svg-data)] + (map #(add-fill {} node (get-svg-data :rect %)) fills))] + (-> props + (assoc :fills fills)))) + + (defn add-svg-content [props node] (let [svg-content (get-data node :penpot:svg-content) @@ -715,37 +749,6 @@ svg-data (or image-data pattern-data)] (:xlink:href svg-data))) -(defn add-library-refs - [props node] - - (let [fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid) - fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid) - stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid) - stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid) - component-id (get-meta node :component-id uuid/uuid) - component-file (get-meta node :component-file uuid/uuid) - shape-ref (get-meta node :shape-ref uuid/uuid) - component-root? (get-meta node :component-root str->bool)] - - (cond-> props - (some? fill-color-ref-id) - (assoc :fill-color-ref-id fill-color-ref-id - :fill-color-ref-file fill-color-ref-file) - - (some? stroke-color-ref-id) - (assoc :stroke-color-ref-id stroke-color-ref-id - :stroke-color-ref-file stroke-color-ref-file) - - (some? component-id) - (assoc :component-id component-id - :component-file component-file) - - component-root? - (assoc :component-root? component-root?) - - (some? shape-ref) - (assoc :shape-ref shape-ref)))) - (defn parse-data [type node] @@ -754,7 +757,6 @@ (-> {} (add-common-data node) (add-position type node svg-data) - (add-fill node svg-data) (add-stroke node svg-data) (add-layer-options svg-data) (add-shadows node) @@ -762,6 +764,7 @@ (add-exports node) (add-svg-attrs node svg-data) (add-library-refs node) + (add-fills node svg-data) (cond-> (= :svg-raw type) (add-svg-content node))