Repository: electron/electron
Pull request: fix: corner smoothing with 0 radii

#### Description of Change

When two adjacent corners have a `border-radius` of 0, a floating-point divide-by-0 edge case is triggered in the corner smoothing code, preventing the element from rendering (but no other side effects).

Added a simple edge case check to resolve this, and also added a fast-path for drawing these corners to avoid any other potential weird behavior.

Added testing coverage for this case + different sized round borders.

#### Checklist

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

I am hoping this will land before 36 goes out, which is the first stable release with corner smoothing. Thus, no release note since the release notes for adding corner smoothing should also be present with this changelog.

Notes: none


Changed files:
filename: patches/chromium/feat_corner_smoothing_css_rule_and_blink_painting.patch
patch: @@ -373,7 +373,7 @@ index 88e78f1d8050c73ae6a8929f23e636ef7a383404..17b63c5ecdcd8feb17b76a13392595c7
  
    // A Corner is a axis-aligned quad, with the points ordered (start, outer,
 diff --git a/third_party/blink/renderer/platform/geometry/path_builder.cc b/third_party/blink/renderer/platform/geometry/path_builder.cc
-index b17bc9760b97b1e72783ae174ffa0f69ca830462..9fcff7b868ca8c29b46d6ed2ddceee3bd8ab9137 100644
+index b17bc9760b97b1e72783ae174ffa0f69ca830462..226b546694524e3848ea849059df74970403690e 100644
 --- a/third_party/blink/renderer/platform/geometry/path_builder.cc
 +++ b/third_party/blink/renderer/platform/geometry/path_builder.cc
 @@ -4,6 +4,7 @@
@@ -384,20 +384,27 @@ index b17bc9760b97b1e72783ae174ffa0f69ca830462..9fcff7b868ca8c29b46d6ed2ddceee3b
  #include "third_party/blink/renderer/platform/geometry/contoured_rect.h"
  #include "third_party/blink/renderer/platform/geometry/infinite_int_rect.h"
  #include "third_party/blink/renderer/platform/geometry/path.h"
-@@ -241,6 +242,19 @@ PathBuilder& PathBuilder::AddContouredRect(
+@@ -241,6 +242,26 @@ PathBuilder& PathBuilder::AddContouredRect(
      AddRoundedRect(target_rect);
      return *this;
    }
 +
 +  // TODO(clavin): decompose `electron::DrawSmoothRoundRect` into corners
 +  if (contoured_rect.GetCornerCurvature().IsSmooth()) {
 +    const gfx::RectF& box = contoured_rect.Rect();
-+    const FloatRoundedRect::Radii& radii = contoured_rect.GetRadii();
++
++    // Constrain the radii (on a copy) to ensure they do not exceed the box.
++    FloatRoundedRect round_rect_copy = contoured_rect.AsRoundedRect();
++    round_rect_copy.ConstrainRadii();
++    const FloatRoundedRect::Radii& radii = round_rect_copy.GetRadii();
++    float smoothness = std::clamp(
++        contoured_rect.GetCornerCurvature().Smoothness(), 0.0f, 1.0f);
++
 +    builder_.addPath(electron::DrawSmoothRoundRect(
-+        box.x(), box.y(), box.width(), box.height(),
-+        std::min(contoured_rect.GetCornerCurvature().Smoothness(), 1.0f),
++        box.x(), box.y(), box.width(), box.height(), smoothness,
 +        radii.TopLeft().width(), radii.TopRight().width(),
 +        radii.BottomRight().width(), radii.BottomLeft().width()));
++
 +    return *this;
 +  }
 +

filename: shell/renderer/electron_smooth_round_rect.cc
patch: @@ -100,10 +100,23 @@ constexpr CurveGeometry::CurveGeometry(float radius, float smoothness) {
 
 void DrawCorner(SkPath& path,
                 float radius,
-                const CurveGeometry& curve1,
-                const CurveGeometry& curve2,
+                float smoothness1,
+                float smoothness2,
                 const SkPoint& corner,
                 unsigned int quarter_rotations) {
+  // If the radius is 0 then we can simply draw a line to the corner point.
+  if (radius == 0.0f) {
+    if (quarter_rotations == 0) {
+      path.moveTo(corner);
+    } else {
+      path.lineTo(corner);
+    }
+    return;
+  }
+
+  CurveGeometry curve1(radius, smoothness1);
+  CurveGeometry curve2(radius, smoothness2);
+
   // Move/Line to the edge connecting point
   {
     SkPoint edge_connecting_point =
@@ -160,6 +173,12 @@ constexpr std::pair<float, float> ConstrainSmoothness(float size,
                                                       float smoothness,
                                                       float radius1,
                                                       float radius2) {
+  // If both radii are 0 then we don't need any smoothing. This avoids a
+  // division by zero in the ratio calculation.
+  if (radius1 == 0.0f && radius2 == 0.0f) {
+    return {0.0f, 0.0f};
+  }
+
   float edge_consumed1 = LengthForCornerSmoothness(smoothness, radius1);
   float edge_consumed2 = LengthForCornerSmoothness(smoothness, radius2);
 
@@ -269,28 +288,20 @@ SkPath DrawSmoothRoundRect(float x,
   SkPath path;
 
   // Top left corner
-  DrawCorner(path, top_left_radius,
-             CurveGeometry(top_left_radius, left_top_smoothness),
-             CurveGeometry(top_left_radius, top_left_smoothness),
+  DrawCorner(path, top_left_radius, left_top_smoothness, top_left_smoothness,
              SkPoint::Make(x, y), 0);
 
   // Top right corner
-  DrawCorner(path, top_right_radius,
-             CurveGeometry(top_right_radius, top_right_smoothness),
-             CurveGeometry(top_right_radius, right_top_smoothness),
+  DrawCorner(path, top_right_radius, top_right_smoothness, right_top_smoothness,
              SkPoint::Make(x + width, y), 1);
 
   // Bottom right corner
-  DrawCorner(path, bottom_right_radius,
-             CurveGeometry(bottom_right_radius, right_bottom_smoothness),
-             CurveGeometry(bottom_right_radius, bottom_right_smoothness),
-             SkPoint::Make(x + width, y + height), 2);
+  DrawCorner(path, bottom_right_radius, right_bottom_smoothness,
+             bottom_right_smoothness, SkPoint::Make(x + width, y + height), 2);
 
   // Bottom left corner
-  DrawCorner(path, bottom_left_radius,
-             CurveGeometry(bottom_left_radius, bottom_left_smoothness),
-             CurveGeometry(bottom_left_radius, left_bottom_smoothness),
-             SkPoint::Make(x, y + height), 3);
+  DrawCorner(path, bottom_left_radius, left_bottom_smoothness,
+             bottom_left_smoothness, SkPoint::Make(x, y + height), 3);
 
   path.close();
   return path;

filename: spec/api-corner-smoothing-spec.ts
patch: @@ -6,7 +6,6 @@ import { AssertionError, expect } from 'chai';
 import path = require('node:path');
 
 import { createArtifact } from './lib/artifacts';
-import { ifdescribe } from './lib/spec-helpers';
 import { closeAllWindows } from './lib/window-helpers';
 
 const FIXTURE_PATH = path.resolve(
@@ -42,7 +41,7 @@ const COMPARISON_TOLERANCE = 2.5;
 function compareImages (img1: NativeImage, img2: NativeImage): boolean {
   expect(img1.getSize()).to.deep.equal(
     img2.getSize(),
-    'Cannot compare images with different sizes'
+    'Cannot compare images with different sizes. Run tests with --force-device-scale-factor=1'
   );
 
   const bitmap1 = img1.toBitmap();
@@ -119,9 +118,7 @@ async function pageCaptureTestRecipe (
   }
 }
 
-// FIXME: these tests rely on live rendering results, which are too variable to
-// reproduce outside of CI, primarily due to display scaling.
-ifdescribe(!!process.env.CI)('-electron-corner-smoothing', () => {
+describe('-electron-corner-smoothing', () => {
   afterEach(async () => {
     await closeAllWindows();
   });

filename: spec/fixtures/api/corner-smoothing/shape/test.html
patch: @@ -44,7 +44,7 @@
       }
 
       .box {
-        --boxes-x: 7;
+        --boxes-x: 8;
         --boxes-y: 5;
         --box-shadow-offset: 4px;
         --box-shadow-spread: 2px;
@@ -84,6 +84,10 @@
         background-color: skyblue;
         box-shadow: var(--box-shadow-offset) var(--box-shadow-offset) var(--box-shadow-spread) var(--box-shadow-grow) cornflowerblue;
       }
+      .box.multiple-radii {
+        background-color: coral;
+        border-radius: 0 0 calc((var(--box-size) / 8) - 2px) calc((var(--box-size) / 2) - 8px);
+      }
     </style>
   </head>
   <body>
@@ -95,6 +99,7 @@
       <div class="box outline double"></div>
       <div class="box outer"><div class="inner"></div></div>
       <div class="box shadow"></div>
+      <div class="box multiple-radii"></div>
     </div>
     <div class="row rounding-30">
       <div class="box"></div>
@@ -104,6 +109,7 @@
       <div class="box outline double"></div>
       <div class="box outer"><div class="inner"></div></div>
       <div class="box shadow"></div>
+      <div class="box multiple-radii"></div>
     </div>
     <div class="row rounding-60">
       <div class="box"></div>
@@ -113,6 +119,7 @@
       <div class="box outline double"></div>
       <div class="box outer"><div class="inner"></div></div>
       <div class="box shadow"></div>
+      <div class="box multiple-radii"></div>
     </div>
     <div class="row rounding-100">
       <div class="box"></div>
@@ -122,6 +129,7 @@
       <div class="box outline double"></div>
       <div class="box outer"><div class="inner"></div></div>
       <div class="box shadow"></div>
+      <div class="box multiple-radii"></div>
     </div>
     <div class="row rounding-invalid">
       <div class="box"></div>
@@ -131,6 +139,7 @@
       <div class="box outline double"></div>
       <div class="box outer"><div class="inner"></div></div>
       <div class="box shadow"></div>
+      <div class="box multiple-radii"></div>
     </div>
   </body>
 </html>
\ No newline at end of file