diff --git a/CHANGES.md b/CHANGES.md index 713eb58eb..2c9e783ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### :boom: Breaking changes ### :sparkles: New features +- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056) - Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660) - Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778) - Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 98102fc94..2b4e9cbc5 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -72,6 +72,7 @@ {:frame #{:proportion-lock :width :height :x :y + :rx :ry :r1 :r2 :r3 :r4 :selrect :opacity diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc index eed914a9c..10bfba88b 100644 --- a/common/src/app/common/pages/init.cljc +++ b/common/src/app/common/pages/init.cljc @@ -73,7 +73,14 @@ :name "Artboard-1" :fills [{:fill-color clr/white :fill-opacity 1}] - :strokes []} + :strokes [] + :stroke-style :none + :stroke-alignment :center + :stroke-width 0 + :stroke-color clr/black + :stroke-opacity 0 + :rx 0 + :ry 0} {:type :text :name "Text-1" diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 38ae85320..f5de551cd 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -27,9 +27,18 @@ [{:keys [shape render-id]}] (when (= :frame (:type shape)) (let [{:keys [x y width height]} shape - padding (filters/calculate-padding shape)] + padding (filters/calculate-padding shape) + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x (- x padding) + :y (- y padding) + :width (+ width (* 2 padding)) + :height (+ height (* 2 padding))})) + path? (some? (.-d props))] [:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"} - [:rect {:x (- x padding) :y (- y padding) :width (+ width (* 2 padding)) :height (+ height (* 2 padding))}]]))) + (if path? + [:> :path props] + [:> :rect props])]))) (mf/defc frame-thumbnail {::mf/wrap-props false} @@ -49,25 +58,28 @@ (defn frame-shape [shape-wrapper] (mf/fnc frame-shape - {::mf/wrap-props false} - [props] - (let [childs (unchecked-get props "childs") - shape (unchecked-get props "shape") - {:keys [x y width height]} shape + {::mf/wrap-props false} + [props] + (let [childs (unchecked-get props "childs") + shape (unchecked-get props "shape") + {:keys [x y width height]} shape - props (-> (attrs/extract-style-attrs shape) - (obj/merge! - #js {:x x - :y y - :width width - :height height - :className "frame-background"}))] + props (-> (attrs/extract-style-attrs shape) + (obj/merge! + #js {:x x + :y y + :width width + :height height + :className "frame-background"})) + path? (some? (.-d props))] + + [:* + [:& shape-custom-strokes {:shape shape} + (if path? + [:> :path props] + [:> :rect props]) - [:* - [:& shape-custom-strokes {:shape shape} - [:> :rect props]] - - (for [item childs] - [:& shape-wrapper {:shape item - :key (dm/str (:id item))}])]))) + (for [item childs] + [:& shape-wrapper {:shape item + :key (dm/str (:id item))}])]]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 7a6493b2d..b7daf4886 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -9,13 +9,12 @@ [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.common.math :as math] - [app.common.spec.radius :as ctr] [app.main.data.workspace :as udw] - [app.main.data.workspace.changes :as dch] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.menus.radius :refer [border-radius]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.alpha :as mf])) @@ -42,8 +41,6 @@ [{:keys [options ids ids-with-children values] :as props}] (let [options (or options #{:size :position :rotation :radius}) - ids-with-children (or ids-with-children ids) - old-shapes (deref (refs/objects-by-id ids)) frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) @@ -63,11 +60,6 @@ proportion-lock (:proportion-lock values) - radius-mode (ctr/radius-mode values) - all-equal? (ctr/all-equal? values) - radius-multi? (mf/use-state nil) - radius-input-ref (mf/use-ref nil) - on-size-change (mf/use-callback (mf/deps ids) @@ -100,64 +92,13 @@ (fn [value] (st/emit! (udw/increase-rotation ids value)))) - on-switch-to-radius-1 - (mf/use-callback - (mf/deps ids) - (fn [_value] - (if all-equal? - (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1)) - (reset! radius-multi? true)))) - - on-switch-to-radius-4 - (mf/use-callback - (mf/deps ids) - (fn [_value] - (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-4)) - (reset! radius-multi? false))) - - on-radius-1-change - (mf/use-callback - (mf/deps ids) - (fn [value] - (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))))) - - on-radius-multi-change - (mf/use-callback - (mf/deps ids) - (fn [event] - (let [value (-> event dom/get-target dom/get-value d/parse-integer)] - (when (some? value) - (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1) - (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))) - (reset! radius-multi? false))))) - - on-radius-4-change - (mf/use-callback - (mf/deps ids) - (fn [value attr] - (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-4 % attr value))))) - on-width-change #(on-size-change % :width) on-height-change #(on-size-change % :height) on-pos-x-change #(on-position-change % :x) on-pos-y-change #(on-position-change % :y) - on-radius-r1-change #(on-radius-4-change % :r1) - on-radius-r2-change #(on-radius-4-change % :r2) - on-radius-r3-change #(on-radius-4-change % :r3) - on-radius-r4-change #(on-radius-4-change % :r4) select-all #(-> % (dom/get-target) (.select))] - (mf/use-layout-effect - (mf/deps radius-mode @radius-multi?) - (fn [] - (when (and (= radius-mode :radius-1) - (= @radius-multi? false)) - ;; when going back from radius-multi to normal radius-1, - ;; restore focus to the newly created numeric-input - (let [radius-input (mf/ref-val radius-input-ref)] - (dom/focus! radius-input))))) - [:* [:div.element-set [:div.element-set-content @@ -233,72 +174,4 @@ :value (attr->string :rotation values)}]]) ;; RADIUS - (when (and (options :radius) (some? radius-mode)) - [:div.row-flex - [:div.radius-options - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected (or (= radius-mode :radius-1) @radius-multi?)) - :alt (tr "workspace.options.radius.all-corners") - :on-click on-switch-to-radius-1} - i/radius-1] - [:div.radius-icon.tooltip.tooltip-bottom - {:class (dom/classnames - :selected (and (= radius-mode :radius-4) (not @radius-multi?))) - :alt (tr "workspace.options.radius.single-corners") - :on-click on-switch-to-radius-4} - i/radius-4]] - - (cond - (= radius-mode :radius-1) - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :ref radius-input-ref - :min 0 - :on-click select-all - :on-change on-radius-1-change - :value (attr->string :rx values)}]] - - @radius-multi? - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:input.input-text - {:type "number" - :placeholder "--" - :on-click select-all - :on-change on-radius-multi-change - :value ""}]] - - (= radius-mode :radius-4) - [:* - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r1-change - :value (attr->string :r1 values)}]] - - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r2-change - :value (attr->string :r2 values)}]] - - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r3-change - :value (attr->string :r3 values)}]] - - [:div.input-element.mini {:title (tr "workspace.options.radius")} - [:> numeric-input - {:placeholder "--" - :min 0 - :on-click select-all - :on-change on-radius-r4-change - :value (attr->string :r4 values)}]]])])]]])) + [:& border-radius {:options options :ids-with-children ids-with-children :values values :ids ids}]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/radius.cljs new file mode 100644 index 000000000..ff2920157 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/radius.cljs @@ -0,0 +1,190 @@ +;; 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 + +;; 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.workspace.sidebar.options.menus.radius + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.math :as math] + [app.common.spec.radius :as ctr] + [app.main.data.workspace.changes :as dch] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + + +(defn- attr->string [attr values] + (let [value (attr values)] + (if (= value :multiple) + "" + (str (-> value + (d/coalesce 0) + (math/precision 2)))))) + +(mf/defc border-radius + [{:keys [options ids ids-with-children values] :as props}] + (let [options (or options #{:size :position :rotation :radius}) + + ids-with-children (or ids-with-children ids) + + old-shapes (deref (refs/objects-by-id ids)) + frames (map #(deref (refs/object-by-id (:frame-id %))) old-shapes) + + shapes (as-> old-shapes $ + (map gsh/transform-shape $) + (map gsh/translate-to-frame $ frames)) + + values (let [{:keys [x y]} (-> shapes first :points gsh/points->selrect)] + (cond-> values + (not= (:x values) :multiple) (assoc :x x) + (not= (:y values) :multiple) (assoc :y y))) + + values (let [{:keys [width height]} (-> shapes first :selrect)] + (cond-> values + (not= (:width values) :multiple) (assoc :width width) + (not= (:height values) :multiple) (assoc :height height))) + + + radius-mode (ctr/radius-mode values) + all-equal? (ctr/all-equal? values) + radius-multi? (mf/use-state nil) + radius-input-ref (mf/use-ref nil) + + on-switch-to-radius-1 + (mf/use-callback + (mf/deps ids) + (fn [_value] + (if all-equal? + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1)) + (reset! radius-multi? true)))) + + on-switch-to-radius-4 + (mf/use-callback + (mf/deps ids) + (fn [_value] + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-4)) + (reset! radius-multi? false))) + + on-radius-1-change + (mf/use-callback + (mf/deps ids) + (fn [value] + (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))))) + + on-radius-multi-change + (mf/use-callback + (mf/deps ids) + (fn [event] + (let [value (-> event dom/get-target dom/get-value d/parse-integer)] + (when (some? value) + (st/emit! (dch/update-shapes ids-with-children ctr/switch-to-radius-1) + (dch/update-shapes ids-with-children #(ctr/set-radius-1 % value))) + (reset! radius-multi? false))))) + + on-radius-4-change + (mf/use-callback + (mf/deps ids) + (fn [value attr] + (st/emit! (dch/update-shapes ids-with-children #(ctr/set-radius-4 % attr value))))) + + on-radius-r1-change #(on-radius-4-change % :r1) + on-radius-r2-change #(on-radius-4-change % :r2) + on-radius-r3-change #(on-radius-4-change % :r3) + on-radius-r4-change #(on-radius-4-change % :r4) + + select-all #(-> % (dom/get-target) (.select))] + + (mf/use-layout-effect + (mf/deps radius-mode @radius-multi?) + (fn [] + (when (and (= radius-mode :radius-1) + (= @radius-multi? false)) + ;; when going back from radius-multi to normal radius-1, + ;; restore focus to the newly created numeric-input + (let [radius-input (mf/ref-val radius-input-ref)] + (dom/focus! radius-input))))) + + [:* + ;; RADIUS + (when (and (options :radius) (some? radius-mode)) + [:div.row-flex + [:div.radius-options + [:div.radius-icon.tooltip.tooltip-bottom + {:class (dom/classnames + :selected (or (= radius-mode :radius-1) @radius-multi?)) + :alt (tr "workspace.options.radius.all-corners") + :on-click on-switch-to-radius-1} + i/radius-1] + [:div.radius-icon.tooltip.tooltip-bottom + {:class (dom/classnames + :selected (and (= radius-mode :radius-4) (not @radius-multi?))) + :alt (tr "workspace.options.radius.single-corners") + :on-click on-switch-to-radius-4} + i/radius-4]] + + (cond + (= radius-mode :radius-1) + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :ref radius-input-ref + :min 0 + :on-click select-all + :on-change on-radius-1-change + :value (attr->string :rx values)}]] + + @radius-multi? + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:input.input-text + {:type "number" + :placeholder "--" + :on-click select-all + :on-change on-radius-multi-change + :value ""}]] + + (= radius-mode :radius-4) + [:* + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r1-change + :value (attr->string :r1 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r2-change + :value (attr->string :r2 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r3-change + :value (attr->string :r3 values)}]] + + [:div.input-element.mini {:title (tr "workspace.options.radius")} + [:> numeric-input + {:placeholder "--" + :min 0 + :on-click select-all + :on-change on-radius-r4-change + :value (attr->string :r4 values)}]]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs index f6965a83e..97b16654a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/frame.cljs @@ -17,6 +17,7 @@ [app.main.ui.workspace.sidebar.options.menus.fill :refer [fill-attrs-shape fill-menu]] [app.main.ui.workspace.sidebar.options.menus.frame-grid :refer [frame-grid]] [app.main.ui.workspace.sidebar.options.menus.layer :refer [layer-attrs layer-menu]] + [app.main.ui.workspace.sidebar.options.menus.radius :refer [border-radius]] [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]] [app.util.dom :as dom] @@ -25,10 +26,12 @@ (declare +size-presets+) + (mf/defc measures-menu [{:keys [shape] :as props}] (let [show-presets-dropdown? (mf/use-state false) + id (:id shape) on-preset-selected (fn [width height] @@ -63,9 +66,7 @@ select-all #(-> % (dom/get-target) (.select))] [:div.element-set - [:div.element-set-content - [:div.row-flex [:div.presets.custom-select.flex-grow {:on-click #(reset! show-presets-dropdown? true)} [:span (tr "workspace.options.size-presets")] @@ -125,7 +126,10 @@ :on-change on-pos-y-change :value (-> (:y shape) (math/precision 2) - (d/coalesce-str "0"))}]]]]])) + (d/coalesce-str "0"))}]]] + + ;; RADIUS + [:& border-radius {:ids [id] :values shape}]]])) (def +size-presets+ [{:name "APPLE"}