From c38117d116f578bef29edb693bff8bcc88779164 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Moya?= <andres.moya@kaleidos.net>
Date: Thu, 18 Feb 2021 16:54:45 +0100
Subject: [PATCH] :tada: Allow a different radius for each rect corner

---
 CHANGES.md                                    |   1 +
 common/app/common/pages/common.cljc           |   4 +
 common/app/common/pages/spec.cljc             |   8 ++
 frontend/resources/images/icons/radius-1.svg  |   3 +
 frontend/resources/images/icons/radius-4.svg  |   3 +
 frontend/resources/locales.json               |  14 ++
 .../resources/styles/common/framework.scss    |   4 +
 .../styles/main/partials/handoff.scss         |   2 +-
 .../partials/sidebar-element-options.scss     |  29 ++++
 .../main/ui/handoff/attributes/layout.cljs    |  23 ++-
 frontend/src/app/main/ui/icons.cljs           |   2 +
 frontend/src/app/main/ui/shapes/attrs.cljs    |  53 ++++++-
 frontend/src/app/main/ui/shapes/rect.cljs     |   5 +-
 .../workspace/sidebar/options/measures.cljs   | 131 ++++++++++++++++--
 frontend/src/app/util/code_gen.cljs           |  38 +++--
 15 files changed, 287 insertions(+), 33 deletions(-)
 create mode 100644 frontend/resources/images/icons/radius-1.svg
 create mode 100644 frontend/resources/images/icons/radius-4.svg

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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+  <path fill-rule="evenodd" clip-rule="evenodd" d="M349.998 50H150C94.77 50 50 94.772 50 150v200c0 55.228 44.771 100 100 100h199.998C405.228 450 450 405.228 450 350V150c0-55.228-44.771-100-100.002-100zM150 0C67.157 0 0 67.157 0 150v200c0 82.844 67.157 150 150 150h199.998C432.84 500 500 432.844 500 350V150C500 67.157 432.841 0 349.998 0z"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+  <path d="M312.498 50h37.5C405.228 50 450 94.772 450 150v37.5h50V150C500 67.157 432.841 0 349.998 0h-37.5zM187.5 50V0H150C67.157 0 0 67.157 0 150v37.5h50V150C50 94.772 94.771 50 150 50zM50 312.5H0V350c0 82.844 67.157 150 150 150h37.5v-50H150C94.77 450 50 405.228 50 350zM312.498 450v50h37.5C432.84 500 500 432.844 500 350v-37.5h-50V350c0 55.228-44.771 100-100.002 100z"/>
+</svg>
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)