diff --git a/CHANGES.md b/CHANGES.md
index b0a3fdee6..79dd23be6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,7 @@
- Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635)
- Disable groups interactions when holding "Ctrl" key (deep selection)
- New action in context menu to "edit" some shapes (binded to key "Enter")
+- Allow to set border radius of each rect corner individually
### :bug: Bugs fixed
diff --git a/common/app/common/pages/common.cljc b/common/app/common/pages/common.cljc
index f56c55bbd..eb5a9572e 100644
--- a/common/app/common/pages/common.cljc
+++ b/common/app/common/pages/common.cljc
@@ -42,6 +42,10 @@
:stroke-alignment :stroke-group
:rx :radius-group
:ry :radius-group
+ :r1 :radius-group
+ :r2 :radius-group
+ :r3 :radius-group
+ :r4 :radius-group
:selrect :geometry-group
:points :geometry-group
:locked :geometry-group
diff --git a/common/app/common/pages/spec.cljc b/common/app/common/pages/spec.cljc
index 25dc321f0..2feef1d71 100644
--- a/common/app/common/pages/spec.cljc
+++ b/common/app/common/pages/spec.cljc
@@ -220,6 +220,10 @@
(s/def :internal.shape/proportion-lock boolean?)
(s/def :internal.shape/rx ::safe-number)
(s/def :internal.shape/ry ::safe-number)
+(s/def :internal.shape/r1 ::safe-number)
+(s/def :internal.shape/r2 ::safe-number)
+(s/def :internal.shape/r3 ::safe-number)
+(s/def :internal.shape/r4 ::safe-number)
(s/def :internal.shape/stroke-color string?)
(s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient))
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
@@ -296,6 +300,10 @@
:internal.shape/proportion-lock
:internal.shape/rx
:internal.shape/ry
+ :internal.shape/r1
+ :internal.shape/r2
+ :internal.shape/r3
+ :internal.shape/r4
:internal.shape/x
:internal.shape/y
:internal.shape/exports
diff --git a/frontend/resources/images/icons/radius-1.svg b/frontend/resources/images/icons/radius-1.svg
new file mode 100644
index 000000000..f1ca422cf
--- /dev/null
+++ b/frontend/resources/images/icons/radius-1.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/radius-4.svg b/frontend/resources/images/icons/radius-4.svg
new file mode 100644
index 000000000..121940d51
--- /dev/null
+++ b/frontend/resources/images/icons/radius-4.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json
index 00c1fcbde..1bc727497 100644
--- a/frontend/resources/locales.json
+++ b/frontend/resources/locales.json
@@ -3640,6 +3640,20 @@
"es" : "Radio"
}
},
+ "workspace.options.radius.all-corners" : {
+ "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
+ "translations" : {
+ "en" : "All corners",
+ "es" : "Todas las esquinas"
+ }
+ },
+ "workspace.options.radius.single-corners" : {
+ "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
+ "translations" : {
+ "en" : "Single corners",
+ "es" : "Esquinas individuales"
+ }
+ },
"workspace.options.rotation" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
"translations" : {
diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss
index fbd547eb9..33116b33d 100644
--- a/frontend/resources/styles/common/framework.scss
+++ b/frontend/resources/styles/common/framework.scss
@@ -385,6 +385,10 @@ ul.slider-dots {
right: 6px;
}
+ &.mini {
+ width: 43px;
+ }
+
// Input amounts
&.pixels {
diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss
index ea94a565a..18267d300 100644
--- a/frontend/resources/styles/main/partials/handoff.scss
+++ b/frontend/resources/styles/main/partials/handoff.scss
@@ -90,7 +90,7 @@
position: relative;
display: flex;
flex-direction: row;
- padding: 1rem 0.5rem;
+ padding: 1rem 1.6rem 1rem 0.5rem;
.attributes-label,
.attributes-value {
diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss
index 866d61eb1..dd6fb3808 100644
--- a/frontend/resources/styles/main/partials/sidebar-element-options.scss
+++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss
@@ -595,6 +595,35 @@
}
+.radius-options {
+ align-items: center;
+ border: 1px solid $color-gray-60;
+ border-radius: 4px;
+ display: flex;
+ justify-content: space-between;
+ padding: 8px;
+ width: 64px;
+
+ .radius-icon {
+ display: flex;
+ align-items: center;
+
+ svg {
+ cursor: pointer;
+ height: 16px;
+ fill: $color-gray-30;
+ width: 16px;
+ }
+
+ &:hover,
+ &.selected {
+ svg {
+ fill: $color-primary;
+ }
+ }
+ }
+}
+
.orientation-icon {
margin-left: $small;
display: flex;
diff --git a/frontend/src/app/main/ui/handoff/attributes/layout.cljs b/frontend/src/app/main/ui/handoff/attributes/layout.cljs
index 02175ece7..c161fffa9 100644
--- a/frontend/src/app/main/ui/handoff/attributes/layout.cljs
+++ b/frontend/src/app/main/ui/handoff/attributes/layout.cljs
@@ -17,13 +17,17 @@
[app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]]))
-(def properties [:width :height :x :y :radius :rx])
+(def properties [:width :height :x :y :radius :rx :r1])
+
(def params
{:to-prop {:x "left"
:y "top"
:rotation "transform"
- :rx "border-radius"}
- :format {:rotation #(str/fmt "rotate(%sdeg)" %)}})
+ :rx "border-radius"
+ :r1 "border-radius"}
+ :format {:rotation #(str/fmt "rotate(%sdeg)" %)
+ :r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %)}
+ :multi {:r1 [:r1 :r2 :r3 :r4]}})
(defn copy-data
([shape]
@@ -62,6 +66,19 @@
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
[:& copy-button {:data (copy-data shape :rx)}]])
+ (when (and (:r1 shape)
+ (or (not= (:r1 shape) 0)
+ (not= (:r2 shape) 0)
+ (not= (:r3 shape) 0)
+ (not= (:r4 shape) 0)))
+ [:div.attributes-unit-row
+ [:div.attributes-label (t locale "handoff.attributes.layout.radius")]
+ [:div.attributes-value (mth/precision (:r1 shape) 2) ", "
+ (mth/precision (:r2 shape) 2) ", "
+ (mth/precision (:r3 shape) 2) ", "
+ (mth/precision (:r4 shape) 2) "px"]
+ [:& copy-button {:data (copy-data shape :r1)}]])
+
(when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 3d54dfb6b..1bf023376 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -87,6 +87,8 @@
(def play (icon-xref :play))
(def plus (icon-xref :plus))
(def radius (icon-xref :radius))
+(def radius-1 (icon-xref :radius-1))
+(def radius-4 (icon-xref :radius-4))
(def recent (icon-xref :recent))
(def redo (icon-xref :redo))
(def rotate (icon-xref :rotate))
diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs
index a548f179c..66c1ac4a7 100644
--- a/frontend/src/app/main/ui/shapes/attrs.cljs
+++ b/frontend/src/app/main/ui/shapes/attrs.cljs
@@ -22,11 +22,56 @@
:dashed "10,10"
nil))
+(defn- truncate-side
+ [shape ra-attr rb-attr dimension-attr]
+ (let [ra (ra-attr shape)
+ rb (rb-attr shape)
+ dimension (dimension-attr shape)]
+ (if (<= (+ ra rb) dimension)
+ [ra rb]
+ [(/ (* ra dimension) (+ ra rb))
+ (/ (* rb dimension) (+ ra rb))])))
+
+(defn- truncate-radius
+ [shape]
+ (let [[r-top-left r-top-right]
+ (truncate-side shape :r1 :r2 :width)
+
+ [r-right-top r-right-bottom]
+ (truncate-side shape :r2 :r3 :height)
+
+ [r-bottom-right r-bottom-left]
+ (truncate-side shape :r3 :r4 :width)
+
+ [r-left-bottom r-left-top]
+ (truncate-side shape :r4 :r1 :height)]
+
+ [(min r-top-left r-left-top)
+ (min r-top-right r-right-top)
+ (min r-right-bottom r-bottom-right)
+ (min r-bottom-left r-left-bottom)]))
+
(defn add-border-radius [attrs shape]
- (if (or (:rx shape) (:ry shape))
- (obj/merge! attrs #js {:rx (:rx shape)
- :ry (:ry shape)})
- attrs))
+ (if (or (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape))
+ (let [[r1 r2 r3 r4] (truncate-radius shape)
+ top (- (:width shape) r1 r2)
+ right (- (:height shape) r2 r3)
+ bottom (- (:width shape) r3 r4)
+ left (- (:height shape) r4 r1)]
+ (obj/merge! attrs #js {:d (str "M" (+ (:x shape) r1) "," (:y shape) " "
+ "h" top " "
+ "a" r2 "," r2 " 0 0 1 " r2 "," r2 " "
+ "v" right " "
+ "a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " "
+ "h" (- bottom) " "
+ "a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " "
+ "v" (- left) " "
+ "a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " "
+ "z")}))
+ (if (or (:rx shape) (:ry shape))
+ (obj/merge! attrs #js {:rx (:rx shape)
+ :ry (:ry shape)})
+ attrs)))
(defn add-fill [attrs shape render-id]
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs
index 555bafa5a..ad3556180 100644
--- a/frontend/src/app/main/ui/shapes/rect.cljs
+++ b/frontend/src/app/main/ui/shapes/rect.cljs
@@ -37,4 +37,7 @@
[:& shape-custom-stroke {:shape shape
:base-props props
- :elem-name "rect"}]))
+ :elem-name
+ (if (.-d props)
+ "path"
+ "rect")}]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/measures.cljs
index 236196506..ec870bd6d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/measures.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/measures.cljs
@@ -24,7 +24,13 @@
[app.common.math :as math]
[app.util.i18n :refer [t] :as i18n]))
-(def measure-attrs [:proportion-lock :width :height :x :y :rotation :rx :ry :selrect])
+(def measure-attrs [:proportion-lock
+ :width :height
+ :x :y
+ :rotation
+ :rx :ry
+ :r1 :r2 :r3 :r4
+ :selrect])
(defn- attr->string [attr values]
(let [value (attr values)]
@@ -93,20 +99,70 @@
(fn [value]
(st/emit! (udw/increase-rotation ids value))))
- on-radius-change
+ on-switch-to-radius-1
(mf/use-callback
(mf/deps ids)
(fn [value]
(let [radius-update
(fn [shape]
(cond-> shape
- (:rx shape) (assoc :rx value :ry value)))]
+ (:r1 shape)
+ (-> (assoc :rx 0 :ry 0)
+ (dissoc :r1 :r2 :r3 :r4))))]
+ (st/emit! (dwc/update-shapes ids-with-children radius-update)))))
+
+ on-switch-to-radius-4
+ (mf/use-callback
+ (mf/deps ids)
+ (fn [value]
+ (let [radius-update
+ (fn [shape]
+ (cond-> shape
+ (:rx shape)
+ (-> (assoc :r1 0 :r2 0 :r3 0 :r4 0)
+ (dissoc :rx :ry))))]
+ (st/emit! (dwc/update-shapes ids-with-children radius-update)))))
+
+ on-radius-1-change
+ (mf/use-callback
+ (mf/deps ids)
+ (fn [value]
+ (let [radius-update
+ (fn [shape]
+ (cond-> shape
+ (:r1 shape)
+ (-> (dissoc :r1 :r2 :r3 :r4)
+ (assoc :rx 0 :ry 0))
+
+ (or (:rx shape) (:r1 shape))
+ (assoc :rx value :ry value)))]
+
+ (st/emit! (dwc/update-shapes ids-with-children radius-update)))))
+
+ on-radius-4-change
+ (mf/use-callback
+ (mf/deps ids)
+ (fn [value attr]
+ (let [radius-update
+ (fn [shape]
+ (cond-> shape
+ (:rx shape)
+ (-> (dissoc :rx :rx)
+ (assoc :r1 0 :r2 0 :r3 0 :r4 0))
+
+ (attr shape)
+ (assoc attr value)))]
+
(st/emit! (dwc/update-shapes ids-with-children radius-update)))))
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))]
[:div.element-set
@@ -181,14 +237,61 @@
:value (attr->string :rotation values)}]])
;; RADIUS
- (when (and (options :radius) (not (nil? (:rx values))))
- [:div.row-flex
- [:span.element-set-subtitle (t locale "workspace.options.radius")]
- [:div.input-element.pixels
- [:> numeric-input
- {:placeholder "--"
- :min 0
- :on-click select-all
- :on-change on-radius-change
- :value (attr->string :rx values)}]]
- [:div.input-element]])]]))
+ (let [radius-1? (some? (:rx values))
+ radius-4? (some? (:r1 values))]
+ (when (and (options :radius) (or radius-1? radius-4?))
+ [:div.row-flex
+ [:div.radius-options
+ [:div.radius-icon.tooltip.tooltip-bottom
+ {:class (classnames
+ :selected
+ (and radius-1? (not radius-4?)))
+ :alt (t locale "workspace.options.radius.all-corners")
+ :on-click on-switch-to-radius-1}
+ i/radius-1]
+ [:div.radius-icon.tooltip.tooltip-bottom
+ {:class (classnames
+ :selected
+ (and radius-4? (not radius-1?)))
+ :alt (t locale "workspace.options.radius.single-corners")
+ :on-click on-switch-to-radius-4}
+ i/radius-4]]
+ (if radius-1?
+ [:div.input-element.mini
+ [:> numeric-input
+ {:placeholder "--"
+ :min 0
+ :on-click select-all
+ :on-change on-radius-1-change
+ :value (attr->string :rx values)}]]
+
+ [:*
+ [:div.input-element.mini
+ [:> numeric-input
+ {:placeholder "--"
+ :min 0
+ :on-click select-all
+ :on-change on-radius-r1-change
+ :value (attr->string :r1 values)}]]
+ [:div.input-element.mini
+ [:> numeric-input
+ {:placeholder "--"
+ :min 0
+ :on-click select-all
+ :on-change on-radius-r2-change
+ :value (attr->string :r2 values)}]]
+ [:div.input-element.mini
+ [:> numeric-input
+ {:placeholder "--"
+ :min 0
+ :on-click select-all
+ :on-change on-radius-r3-change
+ :value (attr->string :r3 values)}]]
+ [:div.input-element.mini
+ [:> 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/util/code_gen.cljs b/frontend/src/app/util/code_gen.cljs
index 54dc6ed78..86e0bd8f5 100644
--- a/frontend/src/app/util/code_gen.cljs
+++ b/frontend/src/app/util/code_gen.cljs
@@ -39,9 +39,15 @@
(str/format "%spx %s %s" width style (uc/color->background color)))))
(def styles-data
- {:layout {:props [:width :height :x :y :radius :rx]
- :to-prop {:x "left" :y "top" :rotation "transform" :rx "border-radius"}
- :format {:rotation #(str/fmt "rotate(%sdeg)" %)}}
+ {:layout {:props [:width :height :x :y :radius :rx :r1]
+ :to-prop {:x "left"
+ :y "top"
+ :rotation "transform"
+ :rx "border-radius"
+ :r1 "border-radius"}
+ :format {:rotation #(str/fmt "rotate(%sdeg)" %)
+ :r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %)}
+ :multi {:r1 [:r1 :r2 :r3 :r4]}}
:fill {:props [:fill-color :fill-color-gradient]
:to-prop {:fill-color "background" :fill-color-gradient "background"}
:format {:fill-color format-fill-color :fill-color-gradient format-fill-color}}
@@ -74,13 +80,14 @@
:text-transform name
:fill-color format-fill-color}})
-
(defn generate-css-props
([values properties]
(generate-css-props values properties nil))
([values properties params]
- (let [{:keys [to-prop format tab-size] :or {to-prop {} tab-size 0}} params
+ (let [{:keys [to-prop format tab-size multi]
+ :or {to-prop {} tab-size 0 multi {}}} params
+
;; We allow the :format and :to-prop to be a map for different properties
;; or just a value for a single property. This code transform a single
;; property to a uniform one
@@ -94,19 +101,28 @@
(into {} (map #(vector % to-prop) properties))
to-prop)
+ get-value (fn [prop]
+ (if-let [props (prop multi)]
+ (map #(get values %) props)
+ (get values prop)))
+
+ null? (fn [value]
+ (if (coll? value)
+ (every? #(or (nil? %) (= % 0)) value)
+ (or (nil? value) (= value 0))))
+
default-format (fn [value] (str (mth/precision value 2) "px"))
format-property (fn [prop]
(let [css-prop (or (prop to-prop) (name prop))
format-fn (or (prop format) default-format)
- css-val (format-fn (prop values) values)]
+ css-val (format-fn (get-value prop) values)]
(when css-val
(str
(str/repeat " " tab-size)
(str/fmt "%s: %s;" css-prop css-val)))))]
(->> properties
- (remove #(let [value (get values %)]
- (or (nil? value) (= value 0))))
+ (remove #(null? (get-value %)))
(map format-property)
(filter (comp not nil?))
(str/join "\n")))))
@@ -114,9 +130,11 @@
(defn shape->properties [shape]
(let [props (->> styles-data vals (mapcat :props))
to-prop (->> styles-data vals (map :to-prop) (reduce merge))
- format (->> styles-data vals (map :format) (reduce merge))]
+ format (->> styles-data vals (map :format) (reduce merge))
+ multi (->> styles-data vals (map :multi) (reduce merge))]
(generate-css-props shape props {:to-prop to-prop
:format format
+ :multi multi
:tab-size 2})))
(defn text->properties [shape]
(let [text-shape-style (select-keys styles-data [:layout :shadow :blur])
@@ -149,7 +167,7 @@
properties (if (= :text (:type shape))
(text->properties shape)
(shape->properties shape))
-
+
selector (str/css-selector name)
selector (if (str/starts-with? selector "-") (subs selector 1) selector)]
(str/join "\n" [(str/fmt "/* %s */" name)