Browse Source

feat: Corner Smoothing CSS rule (#45185)

* feat: Corner Smoothing

* Patch Blink to add CSS rule and Painting

* Add `system-ui` keyword

* Add `cornerSmoothingCSS` web preference

* Add tests

* Documentation

* fixup! Documentation

* fix: initialize smoothness value

* Use a 1.0 scale factor in tests

* Fix tests for CI

* Fix tests

* fixup! Merge branch 'main' into clavin/corner-smoothing

* Add code docs

* Document `system-ui` keyword values

* Address review comments

* fixup! Address review comments

* Address review comments

* Update patch to address upstream changes

The patch went from 694 lines to 505 lines, which is a 27.2% smaller!

* fixup! Update patch to address upstream changes
Calvin 3 weeks ago
parent
commit
b75e802280

+ 2 - 1
docs/README.md

@@ -96,8 +96,9 @@ These individual tutorials expand on topics discussed in the guide above.
 * [Chrome Extensions Support](api/extensions.md)
 * [Breaking API Changes](breaking-changes.md)
 
-### Custom DOM Elements:
+### Custom Web Features:
 
+* [`-electron-corner-smoothing` CSS Rule](api/corner-smoothing-css.md)
 * [`<webview>` Tag](api/webview-tag.md)
 * [`window.open` Function](api/window-open.md)
 

+ 78 - 0
docs/api/corner-smoothing-css.md

@@ -0,0 +1,78 @@
+## CSS Rule: `-electron-corner-smoothing`
+
+> Smoothes out the corner rounding of the `border-radius` CSS rule.
+
+The rounded corners of elements with [the `border-radius` CSS rule](https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius) can be smoothed out using the `-electron-corner-smoothing` CSS rule. This smoothness is very similar to Apple's "continuous" rounded corners in SwiftUI and Figma's "corner smoothing" control on design elements.
+
+![There is a black rectangle on the left using simple rounded corners, and a blue rectangle on the right using smooth rounded corners. In between those rectangles is a magnified view of the same corner from both rectangles overlapping to show the subtle difference in shape.](../images/corner-smoothing-summary.svg)
+
+Integrating with the operating system and its design language is important to many desktop applications. The shape of a rounded corner can be a subtle detail to many users. However, aligning closely to the system's design language that users are familiar with makes the application's design feel familiar too. Beyond matching the design language of macOS, designers may decide to use smoother round corners for many other reasons.
+
+`-electron-corner-smoothing` affects the shape of borders, outlines, and shadows on the target element. Mirroring the behavior of `border-radius`, smoothing will gradually back off if an element's size is too small for the chosen value.
+
+The `-electron-corner-smoothing` CSS rule is **only implemented for Electron** and has no effect in browsers. Avoid using this rule outside of Electron. This CSS rule is considered experimental and may require migration in the future if replaced by a CSS standard.
+
+### Example
+
+The following example shows the effect of corner smoothing at different percents.
+
+```css
+.box {
+  width: 128px;
+  height: 128px;
+  background-color: cornflowerblue;
+  border-radius: 24px;
+  -electron-corner-smoothing: var(--percent);  /* Column header in table below. */
+}
+```
+
+| 0% | 30% | 60% | 100% |
+| --- | --- | --- | --- |
+| ![A rectangle with round corners at 0% smoothness](../images/corner-smoothing-example-0.svg) | ![A rectangle with round corners at 30% smoothness](../images/corner-smoothing-example-30.svg) | ![A rectangle with round corners at 60% smoothness](../images/corner-smoothing-example-60.svg) | ![A rectangle with round corners at 100% smoothness](../images/corner-smoothing-example-100.svg) |
+
+### Matching the system UI
+
+Use the `system-ui` keyword to match the smoothness to the OS design language.
+
+```css
+.box {
+  width: 128px;
+  height: 128px;
+  background-color: cornflowerblue;
+  border-radius: 24px;
+  -electron-corner-smoothing: system-ui;  /* Match the system UI design. */
+}
+```
+
+| OS: | macOS | Windows, Linux |
+| --- | --- | --- |
+| Value: | `60%` | `0%` |
+| Example: | ![A rectangle with round corners whose smoothness matches macOS](../images/corner-smoothing-example-60.svg) | ![A rectangle with round corners whose smoothness matches Windows and Linux](../images/corner-smoothing-example-0.svg) |
+
+### Controlling availibility
+
+This CSS rule can be disabled by setting [the `cornerSmoothingCSS` web preference](./structures/web-preferences.md) to `false`.
+
+```js
+const myWindow = new BrowserWindow({
+  // [...]
+  webPreferences: {
+    enableCornerSmoothingCSS: false // Disables the `-electron-corner-smoothing` CSS rule
+  }
+})
+```
+
+The CSS rule will still parse, but will have no visual effect.
+
+### Formal reference
+
+* **Initial value**: `0%`
+* **Inherited**: No
+* **Animatable**: No
+* **Computed value**: As specified
+
+```css
+-electron-corner-smoothing =
+  <percentage [0,100]>  |
+  system-ui
+```

+ 1 - 0
docs/api/structures/web-preferences.md

@@ -149,6 +149,7 @@
   `WebContents` when the preferred size changes. Default is `false`.
 * `transparent` boolean (optional) - Whether to enable background transparency for the guest page. Default is `true`. **Note:** The guest page's text and background colors are derived from the [color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) of its root element. When transparency is enabled, the text color will still change accordingly but the background will remain transparent.
 * `enableDeprecatedPaste` boolean (optional) _Deprecated_ - Whether to enable the `paste` [execCommand](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand). Default is `false`.
+* `enableCornerSmoothingCSS` boolean (optional) _Experimental_ - Whether the [`-electron-corner-smoothing` CSS rule](../corner-smoothing-css.md) is enabled. Default is `true`.
 
 [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment
 [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5

+ 3 - 0
docs/images/corner-smoothing-example-0.svg

@@ -0,0 +1,3 @@
+<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 48C0 21.4903 21.4903 0 48 0H144C170.51 0 192 21.4903 192 48V144C192 170.51 170.51 192 144 192H48C21.4903 192 0 170.51 0 144V48Z" fill="#6495ED"/>
+</svg>

+ 3 - 0
docs/images/corner-smoothing-example-100.svg

@@ -0,0 +1,3 @@
+<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 96C0 50.7452 0 28.1177 14.0589 14.0589C28.1177 0 50.7452 0 96 0C141.255 0 163.882 0 177.941 14.0589C192 28.1177 192 50.7452 192 96C192 141.255 192 163.882 177.941 177.941C163.882 192 141.255 192 96 192C50.7452 192 28.1177 192 14.0589 177.941C0 163.882 0 141.255 0 96Z" fill="#6495ED"/>
+</svg>

+ 3 - 0
docs/images/corner-smoothing-example-30.svg

@@ -0,0 +1,3 @@
+<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 62.4C0 49.0126 0 42.3188 1.32624 36.7946C5.5399 19.2435 19.2435 5.5399 36.7946 1.32624C42.3188 0 49.0126 0 62.4 0H129.6C142.987 0 149.681 0 155.205 1.32624C172.757 5.5399 186.46 19.2435 190.674 36.7946C192 42.3188 192 49.0126 192 62.4V129.6C192 142.987 192 149.681 190.674 155.205C186.46 172.757 172.757 186.46 155.205 190.674C149.681 192 142.987 192 129.6 192H62.4C49.0126 192 42.3188 192 36.7946 190.674C19.2435 186.46 5.5399 172.757 1.32624 155.205C0 149.681 0 142.987 0 129.6V62.4Z" fill="#6495ED"/>
+</svg>

+ 3 - 0
docs/images/corner-smoothing-example-60.svg

@@ -0,0 +1,3 @@
+<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 76.8C0 49.9175 0 36.4762 5.23169 26.2085C9.83361 17.1767 17.1767 9.83361 26.2085 5.23169C36.4762 0 49.9175 0 76.8 0H115.2C142.083 0 155.524 0 165.792 5.23169C174.823 9.83361 182.166 17.1767 186.768 26.2085C192 36.4762 192 49.9175 192 76.8V115.2C192 142.083 192 155.524 186.768 165.792C182.166 174.823 174.823 182.166 165.792 186.768C155.524 192 142.083 192 115.2 192H76.8C49.9175 192 36.4762 192 26.2085 186.768C17.1767 182.166 9.83361 174.823 5.23169 165.792C0 155.524 0 142.083 0 115.2V76.8Z" fill="#6495ED"/>
+</svg>

+ 15 - 0
docs/images/corner-smoothing-summary.svg

@@ -0,0 +1,15 @@
+<svg width="1024" height="512" viewBox="0 0 1024 512" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="1024" height="512" fill="#EEEEEE"/>
+<rect x="32" y="128" width="256" height="256" fill="white"/>
+<rect x="64" y="160" width="192" height="192" rx="48" stroke="#444444" stroke-width="4"/>
+<rect x="320" y="64" width="384" height="384" fill="white"/>
+<mask id="mask0_1_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="320" y="64" width="384" height="384">
+<rect x="320" y="64" width="384" height="384" fill="white"/>
+</mask>
+<g mask="url(#mask0_1_2)">
+<rect x="85" y="171" width="512" height="512" rx="128" stroke="#444444" stroke-width="8"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M340.677 167H341.323C401.39 167 446.862 167 481.984 171.722C517.284 176.468 542.726 186.05 562.338 205.662C581.95 225.274 591.532 250.716 596.278 286.016C601 321.138 601 366.61 601 426.677V427.323C601 487.39 601 532.862 596.278 567.984C591.532 603.284 581.95 628.726 562.338 648.338C542.726 667.95 517.284 677.532 481.984 682.278C446.862 687 401.39 687 341.323 687H340.677C280.61 687 235.138 687 200.016 682.278C164.716 677.532 139.274 667.95 119.662 648.338C100.05 628.726 90.4679 603.284 85.722 567.984C81 532.862 81 487.39 81 427.323V426.677C81 366.61 81 321.138 85.722 286.016C90.4679 250.716 100.05 225.274 119.662 205.662C139.274 186.05 164.716 176.468 200.016 171.722C235.138 167 280.61 167 340.677 167ZM201.082 179.651C166.67 184.277 143.197 193.441 125.319 211.319C107.441 229.197 98.2773 252.67 93.6506 287.082C89.0085 321.61 89 366.547 89 427C89 487.453 89.0085 532.39 93.6506 566.918C98.2773 601.33 107.441 624.803 125.319 642.681C143.197 660.559 166.67 669.723 201.082 674.349C235.61 678.992 280.547 679 341 679C401.453 679 446.39 678.992 480.918 674.349C515.33 669.723 538.803 660.559 556.681 642.681C574.559 624.803 583.723 601.33 588.349 566.918C592.992 532.39 593 487.453 593 427C593 366.547 592.992 321.61 588.349 287.082C583.723 252.67 574.559 229.197 556.681 211.319C538.803 193.441 515.33 184.277 480.918 179.651C446.39 175.008 401.453 175 341 175C280.547 175 235.61 175.008 201.082 179.651Z" fill="#2A90D9"/>
+</g>
+<rect x="731" y="128" width="256" height="256" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M863.839 158H864.161C886.652 158 903.732 158 916.936 159.775C930.228 161.562 939.892 165.182 947.355 172.645C954.818 180.108 958.438 189.772 960.225 203.064C962 216.268 962 233.348 962 255.839V256.161C962 278.652 962 295.732 960.225 308.936C958.438 322.228 954.818 331.892 947.355 339.355C939.892 346.818 930.228 350.438 916.936 352.225C903.732 354 886.652 354 864.161 354H863.839C841.348 354 824.268 354 811.064 352.225C797.772 350.438 788.108 346.818 780.645 339.355C773.182 331.892 769.562 322.228 767.775 308.936C766 295.732 766 278.652 766 256.161V255.839C766 233.348 766 216.268 767.775 203.064C769.562 189.772 773.182 180.108 780.645 172.645C788.108 165.182 797.772 161.562 811.064 159.775C824.268 158 841.348 158 863.839 158ZM811.597 163.74C798.748 165.467 790.069 168.877 783.473 175.473C776.877 182.069 773.467 190.748 771.74 203.597C770.004 216.504 770 233.316 770 256C770 278.684 770.004 295.496 771.74 308.403C773.467 321.252 776.877 329.931 783.473 336.527C790.069 343.123 798.748 346.533 811.597 348.26C824.504 349.996 841.316 350 864 350C886.684 350 903.496 349.996 916.403 348.26C929.252 346.533 937.931 343.123 944.527 336.527C951.123 329.931 954.533 321.252 956.26 308.403C957.996 295.496 958 278.684 958 256C958 233.316 957.996 216.504 956.26 203.597C954.533 190.748 951.123 182.069 944.527 175.473C937.931 168.877 929.252 165.467 916.403 163.74C903.496 162.004 886.684 162 864 162C841.316 162 824.504 162.004 811.597 163.74Z" fill="#2A90D9"/>
+</svg>

+ 1 - 0
filenames.auto.gni

@@ -14,6 +14,7 @@ auto_filenames = {
     "docs/api/content-tracing.md",
     "docs/api/context-bridge.md",
     "docs/api/cookies.md",
+    "docs/api/corner-smoothing-css.md",
     "docs/api/crash-reporter.md",
     "docs/api/debugger.md",
     "docs/api/desktop-capturer.md",

+ 2 - 0
filenames.gni

@@ -718,6 +718,8 @@ filenames = {
     "shell/renderer/electron_renderer_client.h",
     "shell/renderer/electron_sandboxed_renderer_client.cc",
     "shell/renderer/electron_sandboxed_renderer_client.h",
+    "shell/renderer/electron_smooth_round_rect.cc",
+    "shell/renderer/electron_smooth_round_rect.h",
     "shell/renderer/preload_realm_context.cc",
     "shell/renderer/preload_realm_context.h",
     "shell/renderer/preload_utils.cc",

+ 1 - 0
patches/chromium/.patches

@@ -134,6 +134,7 @@ fix_software_compositing_infinite_loop.patch
 fix_add_method_which_disables_headless_mode_on_native_widget.patch
 refactor_unfilter_unresponsive_events.patch
 build_disable_thin_lto_mac.patch
+feat_corner_smoothing_css_rule_and_blink_painting.patch
 build_add_public_config_simdutf_config.patch
 revert_code_health_clean_up_stale_macwebcontentsocclusion.patch
 ignore_parse_errors_for_pkey_appusermodel_toastactivatorclsid.patch

+ 522 - 0
patches/chromium/feat_corner_smoothing_css_rule_and_blink_painting.patch

@@ -0,0 +1,522 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Calvin Watford <[email protected]>
+Date: Mon, 9 Dec 2024 16:58:15 -0700
+Subject: feat: Corner Smoothing CSS rule and Blink painting
+
+This patch implements the `-electron-corner-smoothing` CSS rule by
+making three primary changes to Blink:
+
+1. Adds the `-electron-corner-smoothing` CSS rule:
+    * Metadata in `blink/renderer/core/css/css_properties.json5`
+    * Parsing in `blink/renderer/core/css/properties/longhands/longhands_custom.cc`
+    * Other required definitions for all CSS rules (`css_property_id.mojom`, `css_property_equality.cc`)
+
+2. Modifies how Blink paints rounded rectangles:
+    * Augments `blink::FloatRoundedRect` to store smoothness & hook into
+      drawing smooth corners.
+    * Modifies all call sites of `blink::FloatRoundedRect::operator SkRRect`
+      to identify and use smooth rounded corners.
+
+3. Adds a renderer preference / web setting:
+    * Controls whether the CSS rule is available.
+
+diff --git a/third_party/blink/common/renderer_preferences/renderer_preferences_mojom_traits.cc b/third_party/blink/common/renderer_preferences/renderer_preferences_mojom_traits.cc
+index 25cf6b544dcee15a9616b6963eaae0264aba3db6..13d5b30d00ce8dca96eb3bc5454f9d353375d4c6 100644
+--- a/third_party/blink/common/renderer_preferences/renderer_preferences_mojom_traits.cc
++++ b/third_party/blink/common/renderer_preferences/renderer_preferences_mojom_traits.cc
+@@ -128,6 +128,8 @@ bool StructTraits<blink::mojom::RendererPreferencesDataView,
+     return false;
+   }
+ 
++  out->electron_corner_smoothing_css = data.electron_corner_smoothing_css();
++
+   return true;
+ }
+ 
+diff --git a/third_party/blink/public/common/renderer_preferences/renderer_preferences.h b/third_party/blink/public/common/renderer_preferences/renderer_preferences.h
+index cae096396b0635f1c4bba6ac8fee47fd957dc698..03db6cddab5cd1b9f3f7c90390bc53baa9e14b65 100644
+--- a/third_party/blink/public/common/renderer_preferences/renderer_preferences.h
++++ b/third_party/blink/public/common/renderer_preferences/renderer_preferences.h
+@@ -91,6 +91,7 @@ struct BLINK_COMMON_EXPORT RendererPreferences {
+   bool caret_browsing_enabled{false};
+   bool uses_platform_autofill{false};
+   std::vector<uint16_t> explicitly_allowed_network_ports;
++  bool electron_corner_smoothing_css;
+ 
+   RendererPreferences();
+   RendererPreferences(const RendererPreferences& other);
+diff --git a/third_party/blink/public/common/renderer_preferences/renderer_preferences_mojom_traits.h b/third_party/blink/public/common/renderer_preferences/renderer_preferences_mojom_traits.h
+index 33b4bd3f0c9488f1013aea026c7fe559ba750cd8..6b4157199c14a4c276e65512e89f2429253aec5c 100644
+--- a/third_party/blink/public/common/renderer_preferences/renderer_preferences_mojom_traits.h
++++ b/third_party/blink/public/common/renderer_preferences/renderer_preferences_mojom_traits.h
+@@ -275,6 +275,11 @@ struct BLINK_COMMON_EXPORT
+     return data.explicitly_allowed_network_ports;
+   }
+ 
++  static const bool& electron_corner_smoothing_css(
++      const ::blink::RendererPreferences& data) {
++    return data.electron_corner_smoothing_css;
++  }
++
+   static bool Read(blink::mojom::RendererPreferencesDataView,
+                    ::blink::RendererPreferences* out);
+ };
+diff --git a/third_party/blink/public/mojom/renderer_preferences.mojom b/third_party/blink/public/mojom/renderer_preferences.mojom
+index bbcec1dcdaaaf932b3d82c64e8aeb2e7c04b05bf..689205607a763c1d6e040069b1357d84e8ba4bd5 100644
+--- a/third_party/blink/public/mojom/renderer_preferences.mojom
++++ b/third_party/blink/public/mojom/renderer_preferences.mojom
+@@ -201,4 +201,6 @@ struct RendererPreferences {
+   bool uses_platform_autofill = false;
+ 
+   array<uint16> explicitly_allowed_network_ports;
++
++  bool electron_corner_smoothing_css;
+ };
+diff --git a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
+index ff34492a1b3e0d57847a35f5921337debdf14d10..02e77d7f080de9311f8ac70e3e935dbd38cb285f 100644
+--- a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
++++ b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
+@@ -48,6 +48,7 @@ enum CSSSampleId {
+     kInternalForcedVisitedColor = 0,
+     kInternalOverflowBlock = 0,
+     kInternalOverflowInline = 0,
++    kElectronCornerSmoothing = 0,
+ 
+     // This CSSSampleId represents page load for CSS histograms. It is recorded once
+     // per page visit for each CSS histogram being logged on the blink side and the
+diff --git a/third_party/blink/public/web/web_settings.h b/third_party/blink/public/web/web_settings.h
+index 0255879e4ae048554041ca03891fbc7ba8484187..8248d8f54eb5d35caa39a306a6477ace6597b03a 100644
+--- a/third_party/blink/public/web/web_settings.h
++++ b/third_party/blink/public/web/web_settings.h
+@@ -284,6 +284,7 @@ class WebSettings {
+   virtual void SetModalContextMenu(bool) = 0;
+   virtual void SetRequireTransientActivationAndAuthorizationForSubAppsAPIs(
+       bool) = 0;
++  virtual void SetCornerSmoothingCSS(bool) = 0;
+ 
+  protected:
+   ~WebSettings() = default;
+diff --git a/third_party/blink/renderer/build/scripts/core/css/css_properties.py b/third_party/blink/renderer/build/scripts/core/css/css_properties.py
+index 753ba8990f722bafd1770a5e70307cff3764d3f1..16cec517d72887c089f85867e8e37c03199ab394 100755
+--- a/third_party/blink/renderer/build/scripts/core/css/css_properties.py
++++ b/third_party/blink/renderer/build/scripts/core/css/css_properties.py
+@@ -311,7 +311,13 @@ class CSSProperties(object):
+             name_without_leading_dash = property_.name.original
+             if name_without_leading_dash.startswith('-'):
+                 name_without_leading_dash = name_without_leading_dash[1:]
++            # Extra sort level to avoid -internal-* properties being assigned
++            # values too large to fit in a byte.
++            internal_weight = 0
++            if property_.name.original.startswith('-internal'):
++                internal_weight = -1
+             property_.sorting_key = (-property_.priority,
++                                     internal_weight,
+                                      name_without_leading_dash)
+ 
+         sorting_keys = {}
+diff --git a/third_party/blink/renderer/core/css/css_properties.json5 b/third_party/blink/renderer/core/css/css_properties.json5
+index 561cf75a5fc16e94485296e4607df70037e53525..23ece6535cab358fdc8f9dfc1481c1a020475daf 100644
+--- a/third_party/blink/renderer/core/css/css_properties.json5
++++ b/third_party/blink/renderer/core/css/css_properties.json5
+@@ -8732,6 +8732,24 @@
+       property_methods: ["ParseShorthand", "CSSValueFromComputedStyleInternal"],
+     },
+ 
++    {
++      name: "-electron-corner-smoothing",
++      property_methods: ["ParseSingleValue"],
++      field_group: "*",
++      field_template: "external",
++      // To keep this patch small, Length is used instead of a more descriptive
++      // custom type.
++      // - `system-ui` = `Length::Auto()`
++      // - percent     = `Length::Percent`
++      type_name: "Length",
++      converter: "ConvertCornerSmoothing",
++      keywords: ["system-ui"],
++      default_value: "Length::None()",
++      typedom_types: ["Keyword", "Percentage"],
++      is_border_radius: true,
++      invalidate: ["paint", "border-radius", "clip"],
++    },
++
+     // Visited properties.
+     {
+       name: "-internal-visited-color",
+diff --git a/third_party/blink/renderer/core/css/css_property_equality.cc b/third_party/blink/renderer/core/css/css_property_equality.cc
+index d0c6c6b54dd773c6d3f8b9767b126689198df4ee..2348a1d8ebb1d734ee8cea32b9d67adf3f5fb605 100644
+--- a/third_party/blink/renderer/core/css/css_property_equality.cc
++++ b/third_party/blink/renderer/core/css/css_property_equality.cc
+@@ -346,6 +346,8 @@ bool CSSPropertyEquality::PropertiesEqual(const PropertyHandle& property,
+       return a.DominantBaseline() == b.DominantBaseline();
+     case CSSPropertyID::kDynamicRangeLimit:
+       return a.GetDynamicRangeLimit() == b.GetDynamicRangeLimit();
++    case CSSPropertyID::kElectronCornerSmoothing:
++      return a.ElectronCornerSmoothing() == b.ElectronCornerSmoothing();
+     case CSSPropertyID::kEmptyCells:
+       return a.EmptyCells() == b.EmptyCells();
+     case CSSPropertyID::kFill:
+diff --git a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
+index b1b513c8d89055f3e77d406f0d889243b5e7ad7f..2b0a37f496fad6b7b56896cc1811f624e89e9aad 100644
+--- a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
++++ b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
+@@ -11826,5 +11826,25 @@ const CSSValue* InternalEmptyLineHeight::ParseSingleValue(
+                                          CSSValueID::kNone>(stream);
+ }
+ 
++const CSSValue* ElectronCornerSmoothing::ParseSingleValue(
++    CSSParserTokenStream& stream,
++    const CSSParserContext& context,
++    const CSSParserLocalContext&) const {
++  // Fail parsing if this rule is disabled by document settings.
++  if (Settings* settings = context.GetDocument()->GetSettings();
++      settings && !settings->GetElectronCornerSmoothingCSS()) {
++    return nullptr;
++  }
++
++  // Try to parse `system-ui` keyword first.
++  if (auto* ident =
++          css_parsing_utils::ConsumeIdent<CSSValueID::kSystemUi>(stream)) {
++    return ident;
++  }
++  // Try to parse as percent.
++  return css_parsing_utils::ConsumePercent(
++      stream, context, CSSPrimitiveValue::ValueRange::kNonNegative);
++}
++
+ }  // namespace css_longhand
+ }  // namespace blink
+diff --git a/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc b/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
+index 26222ba06232da5d98b35173a7ab0e55a3e6d076..92cdc2aed6a68581187ae654358cc7f2b2409cb3 100644
+--- a/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
++++ b/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
+@@ -3875,4 +3875,12 @@ PositionArea StyleBuilderConverter::ConvertPositionArea(
+   return PositionArea(span[0], span[1], span[2], span[3]);
+ }
+ 
++Length StyleBuilderConverter::ConvertCornerSmoothing(StyleResolverState& state, const CSSValue& value) {
++  auto* ident = DynamicTo<CSSIdentifierValue>(value);
++  if (ident && ident->GetValueID() == CSSValueID::kSystemUi) {
++    return Length::Auto();
++  }
++  return ConvertLength(state, value);
++}
++
+ }  // namespace blink
+diff --git a/third_party/blink/renderer/core/css/resolver/style_builder_converter.h b/third_party/blink/renderer/core/css/resolver/style_builder_converter.h
+index c0f4544a38dc486708dec5a4b3646fb3f15ff2e0..8b3d4e95fb690f9e7b38265be0a77d6e49271944 100644
+--- a/third_party/blink/renderer/core/css/resolver/style_builder_converter.h
++++ b/third_party/blink/renderer/core/css/resolver/style_builder_converter.h
+@@ -421,6 +421,8 @@ class StyleBuilderConverter {
+                                                  const CSSValue&);
+ 
+   static PositionArea ConvertPositionArea(StyleResolverState&, const CSSValue&);
++
++  static Length ConvertCornerSmoothing(StyleResolverState&, const CSSValue&);
+ };
+ 
+ template <typename T>
+diff --git a/third_party/blink/renderer/core/exported/web_settings_impl.cc b/third_party/blink/renderer/core/exported/web_settings_impl.cc
+index 673bafce059a21da812155ef1c6c5577fe211a91..40dfafd5828cac131bb1b4bbc0c8d64f2d09d4f2 100644
+--- a/third_party/blink/renderer/core/exported/web_settings_impl.cc
++++ b/third_party/blink/renderer/core/exported/web_settings_impl.cc
+@@ -809,4 +809,8 @@ void WebSettingsImpl::
+       is_required);
+ }
+ 
++void WebSettingsImpl::SetCornerSmoothingCSS(bool available) {
++  settings_->SetElectronCornerSmoothingCSS(available);
++}
++
+ }  // namespace blink
+diff --git a/third_party/blink/renderer/core/exported/web_settings_impl.h b/third_party/blink/renderer/core/exported/web_settings_impl.h
+index ea27e0cfabfec4cba6d39b4dbfbaca59cae84f8e..48abf7909b9e6b095d431739094bf8145282a01c 100644
+--- a/third_party/blink/renderer/core/exported/web_settings_impl.h
++++ b/third_party/blink/renderer/core/exported/web_settings_impl.h
+@@ -236,6 +236,7 @@ class CORE_EXPORT WebSettingsImpl final : public WebSettings {
+   void SetModalContextMenu(bool) override;
+   void SetRequireTransientActivationAndAuthorizationForSubAppsAPIs(
+       bool) override;
++  void SetCornerSmoothingCSS(bool) override;
+ 
+   bool RenderVSyncNotificationEnabled() const {
+     return render_v_sync_notification_enabled_;
+diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc
+index 64f243d595920a87e7115ca8047822a7368752df..1a1b92ae3dd892413c76d3d61a41d33736c04d9f 100644
+--- a/third_party/blink/renderer/core/exported/web_view_impl.cc
++++ b/third_party/blink/renderer/core/exported/web_view_impl.cc
+@@ -3579,6 +3579,9 @@ void WebViewImpl::UpdateRendererPreferences(
+ #endif
+ 
+   MaybePreloadSystemFonts(GetPage());
++
++  GetSettings()->SetCornerSmoothingCSS(
++      renderer_preferences_.electron_corner_smoothing_css);
+ }
+ 
+ void WebViewImpl::SetHistoryIndexAndLength(int32_t history_index,
+diff --git a/third_party/blink/renderer/core/frame/settings.json5 b/third_party/blink/renderer/core/frame/settings.json5
+index 6566580ef5e20ac5a66507e6ef625db9258ca906..b36ed88cf2f8c3e1554803caf9fd31c8ac70f733 100644
+--- a/third_party/blink/renderer/core/frame/settings.json5
++++ b/third_party/blink/renderer/core/frame/settings.json5
+@@ -1252,5 +1252,10 @@
+       initial: false,
+       type: "bool"
+     },
++    {
++      name: "electronCornerSmoothingCSS",
++      initial: true,
++      invalidate: ["Style"],
++    },
+   ],
+ }
+diff --git a/third_party/blink/renderer/core/paint/box_border_painter.cc b/third_party/blink/renderer/core/paint/box_border_painter.cc
+index 7996ba931a2482e9941ce4642b2c017c294fe89d..abc87db736b47b1bbaa8b9717b0520a747b1725f 100644
+--- a/third_party/blink/renderer/core/paint/box_border_painter.cc
++++ b/third_party/blink/renderer/core/paint/box_border_painter.cc
+@@ -246,7 +246,7 @@ FloatRoundedRect CalculateAdjustedInnerBorder(
+       break;
+   }
+ 
+-  return FloatRoundedRect(new_rect, new_radii);
++  return FloatRoundedRect(new_rect, new_radii, inner_border.GetSmoothness());
+ }
+ 
+ void DrawSolidBorderRect(GraphicsContext& context,
+diff --git a/third_party/blink/renderer/core/paint/box_painter_base.cc b/third_party/blink/renderer/core/paint/box_painter_base.cc
+index 5c3e86f6476c58e771b16b485c4f5c0081af3f7b..6e49fcce3c7b6a9664a4a1bd63200fdb85d05980 100644
+--- a/third_party/blink/renderer/core/paint/box_painter_base.cc
++++ b/third_party/blink/renderer/core/paint/box_painter_base.cc
+@@ -319,7 +319,8 @@ void BoxPainterBase::PaintNormalBoxShadow(const PaintInfo& info,
+     context.SetDrawLooper(draw_looper_builder.DetachDrawLooper());
+ 
+     if (has_border_radius) {
+-      FloatRoundedRect rounded_fill_rect(fill_rect, border.GetRadii());
++      FloatRoundedRect rounded_fill_rect(fill_rect, border.GetRadii(),
++                                         border.GetSmoothness());
+       ApplySpreadToShadowShape(rounded_fill_rect, shadow.Spread());
+       context.FillRoundedRect(
+           rounded_fill_rect, Color::kBlack,
+@@ -403,7 +404,8 @@ void BoxPainterBase::PaintInsetBoxShadow(const PaintInfo& info,
+ 
+     gfx::RectF inner_rect = bounds.Rect();
+     AdjustRectForSideClipping(inner_rect, shadow, sides_to_include);
+-    FloatRoundedRect inner_rounded_rect(inner_rect, bounds.GetRadii());
++    FloatRoundedRect inner_rounded_rect(inner_rect, bounds.GetRadii(),
++                                        bounds.GetSmoothness());
+     ApplySpreadToShadowShape(inner_rounded_rect, -shadow.Spread());
+     if (inner_rounded_rect.IsEmpty()) {
+       // |AutoDarkMode::Disabled()| is used because |shadow_color| has already
+diff --git a/third_party/blink/renderer/core/paint/rounded_border_geometry.cc b/third_party/blink/renderer/core/paint/rounded_border_geometry.cc
+index 7f4026755c285897a0892edfa40a91201a8a4830..795cece5a853662ede5a7439d17b4bee67d51fff 100644
+--- a/third_party/blink/renderer/core/paint/rounded_border_geometry.cc
++++ b/third_party/blink/renderer/core/paint/rounded_border_geometry.cc
+@@ -52,6 +52,24 @@ FloatRoundedRect::CornerCurvature CalcCurvatureFor(
+       EffectiveCurvature(style.CornerBottomLeftShape(), radii.BottomLeft()));
+ }
+ 
++float SmoothnessFromLength(const Length& length) {
++  // `none` = 0%
++  if (length.IsNone()) {
++    return 0.0f;
++  }
++
++  // `system-ui` keyword, represented internally as "auto" length
++  if (length.HasAuto()) {
++#if BUILDFLAG(IS_MAC)
++    return 0.6f;
++#else
++    return 0.0f;
++#endif  // BUILDFLAG(IS_MAC)
++  }
++
++  return length.Percent() / 100.0f;
++}
++
+ FloatRoundedRect PixelSnappedRoundedBorderInternal(
+     const ComputedStyle& style,
+     const PhysicalRect& border_rect,
+@@ -64,6 +82,8 @@ FloatRoundedRect PixelSnappedRoundedBorderInternal(
+     rounded_rect.ConstrainRadii();
+     rounded_rect.SetCornerCurvature(
+         CalcCurvatureFor(style, rounded_rect.GetRadii()));
++    rounded_rect.SetSmoothness(
++        SmoothnessFromLength(style.ElectronCornerSmoothing()));
+   }
+   return rounded_rect;
+ }
+@@ -80,6 +100,8 @@ FloatRoundedRect RoundedBorderGeometry::RoundedBorder(
+     rounded_rect.ConstrainRadii();
+     rounded_rect.SetCornerCurvature(
+         CalcCurvatureFor(style, rounded_rect.GetRadii()));
++    rounded_rect.SetSmoothness(
++        SmoothnessFromLength(style.ElectronCornerSmoothing()));
+   }
+   return rounded_rect;
+ }
+@@ -167,6 +189,8 @@ FloatRoundedRect RoundedBorderGeometry::RoundedInnerBorder(
+                            .set_right(style.BorderRightWidth())
+                            .set_bottom(style.BorderBottomWidth())
+                            .set_left(style.BorderLeftWidth()));
++  rounded_border.SetSmoothness(
++      SmoothnessFromLength(style.ElectronCornerSmoothing()));
+   return rounded_border;
+ }
+ 
+@@ -214,6 +238,7 @@ FloatRoundedRect RoundedBorderGeometry::PixelSnappedRoundedBorderWithOutsets(
+     rounded_rect.SetRadii(pixel_snapped_rounded_border.GetRadii());
+     rounded_rect.SetCornerCurvature(
+         pixel_snapped_rounded_border.GetCornerCurvature());
++    rounded_rect.SetSmoothness(pixel_snapped_rounded_border.GetSmoothness());
+   }
+   return rounded_rect;
+ }
+diff --git a/third_party/blink/renderer/core/paint/rounded_inner_rect_clipper.cc b/third_party/blink/renderer/core/paint/rounded_inner_rect_clipper.cc
+index f9f0cc13c043e4653d8a616d0ef4f2f776aace74..6217ca2a6c507c90980be54b1e3d12ce621be099 100644
+--- a/third_party/blink/renderer/core/paint/rounded_inner_rect_clipper.cc
++++ b/third_party/blink/renderer/core/paint/rounded_inner_rect_clipper.cc
+@@ -28,16 +28,16 @@ RoundedInnerRectClipper::RoundedInnerRectClipper(
+                             rect.Bottom() - clip_rect.Rect().y());
+       FloatRoundedRect::Radii top_corner_radii;
+       top_corner_radii.SetTopLeft(clip_rect.GetRadii().TopLeft());
+-      rounded_rect_clips.push_back(
+-          FloatRoundedRect(top_corner, top_corner_radii));
++      rounded_rect_clips.push_back(FloatRoundedRect(
++          top_corner, top_corner_radii, clip_rect.GetSmoothness()));
+ 
+       gfx::RectF bottom_corner(rect.X().ToFloat(), rect.Y().ToFloat(),
+                                clip_rect.Rect().right() - rect.X().ToFloat(),
+                                clip_rect.Rect().bottom() - rect.Y().ToFloat());
+       FloatRoundedRect::Radii bottom_corner_radii;
+       bottom_corner_radii.SetBottomRight(clip_rect.GetRadii().BottomRight());
+-      rounded_rect_clips.push_back(
+-          FloatRoundedRect(bottom_corner, bottom_corner_radii));
++      rounded_rect_clips.push_back(FloatRoundedRect(
++          bottom_corner, bottom_corner_radii, clip_rect.GetSmoothness()));
+     }
+ 
+     if (!clip_rect.GetRadii().TopRight().IsEmpty() ||
+@@ -47,16 +47,16 @@ RoundedInnerRectClipper::RoundedInnerRectClipper(
+                             rect.Bottom() - clip_rect.Rect().y());
+       FloatRoundedRect::Radii top_corner_radii;
+       top_corner_radii.SetTopRight(clip_rect.GetRadii().TopRight());
+-      rounded_rect_clips.push_back(
+-          FloatRoundedRect(top_corner, top_corner_radii));
++      rounded_rect_clips.push_back(FloatRoundedRect(
++          top_corner, top_corner_radii, clip_rect.GetSmoothness()));
+ 
+       gfx::RectF bottom_corner(clip_rect.Rect().x(), rect.Y().ToFloat(),
+                                rect.Right() - clip_rect.Rect().x(),
+                                clip_rect.Rect().bottom() - rect.Y().ToFloat());
+       FloatRoundedRect::Radii bottom_corner_radii;
+       bottom_corner_radii.SetBottomLeft(clip_rect.GetRadii().BottomLeft());
+-      rounded_rect_clips.push_back(
+-          FloatRoundedRect(bottom_corner, bottom_corner_radii));
++      rounded_rect_clips.push_back(FloatRoundedRect(
++          bottom_corner, bottom_corner_radii, clip_rect.GetSmoothness()));
+     }
+   }
+ 
+diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
+index 8546d85128cc970cb2fd7b0b98642c1403fa5c7d..59f503cbde358d999ffc3a8499a94e1f785d7bed 100644
+--- a/third_party/blink/renderer/platform/BUILD.gn
++++ b/third_party/blink/renderer/platform/BUILD.gn
+@@ -1634,6 +1634,8 @@ component("platform") {
+     "widget/widget_base.h",
+     "widget/widget_base_client.h",
+     "windows_keyboard_codes.h",
++    "//electron/shell/renderer/electron_smooth_round_rect.h",
++    "//electron/shell/renderer/electron_smooth_round_rect.cc",
+   ]
+ 
+   sources -= blink_platform_avx_files
+diff --git a/third_party/blink/renderer/platform/geometry/float_rounded_rect.h b/third_party/blink/renderer/platform/geometry/float_rounded_rect.h
+index 7bf6608b57f8fc2283026dac6bff0c81bca380fd..09f34525343dacacf966f7ab4918ee57ae0c1470 100644
+--- a/third_party/blink/renderer/platform/geometry/float_rounded_rect.h
++++ b/third_party/blink/renderer/platform/geometry/float_rounded_rect.h
+@@ -197,15 +197,19 @@ class PLATFORM_EXPORT FloatRoundedRect {
+       : rect_(rect), radii_(radius) {}
+   FloatRoundedRect(const gfx::RectF& r, float radius_x, float radius_y)
+       : FloatRoundedRect(r, Radii(radius_x, radius_y)) {}
++  FloatRoundedRect(const gfx::RectF& r, const Radii& radii, float smoothness)
++      : rect_(r), radii_(radii), smoothness_(smoothness) {}
+ 
+   constexpr const gfx::RectF& Rect() const { return rect_; }
+   constexpr const Radii& GetRadii() const { return radii_; }
+   constexpr const CornerCurvature& GetCornerCurvature() const {
+     return corner_curvature_;
+   }
++  constexpr float GetSmoothness() const { return smoothness_; }
+   constexpr bool IsRounded() const { return !radii_.IsZero(); }
++  constexpr bool IsSmooth() const { return smoothness_ > 0.0f; }
+   constexpr bool HasSimpleRoundedCurvature() const {
+-    return !IsRounded() || corner_curvature_.IsRound();
++    return !IsRounded() || (corner_curvature_.IsRound() && !IsSmooth());
+   }
+   constexpr bool IsEmpty() const { return rect_.IsEmpty(); }
+ 
+@@ -214,6 +218,7 @@ class PLATFORM_EXPORT FloatRoundedRect {
+   void SetCornerCurvature(const CornerCurvature& curvature) {
+     corner_curvature_ = curvature;
+   }
++  void SetSmoothness(float smoothness) { smoothness_ = smoothness; }
+ 
+   void Move(const gfx::Vector2dF& offset) { rect_.Offset(offset); }
+ 
+@@ -297,6 +302,7 @@ class PLATFORM_EXPORT FloatRoundedRect {
+   gfx::RectF rect_;
+   Radii radii_;
+   CornerCurvature corner_curvature_;
++  float smoothness_ = 0.0f;
+ };
+ 
+ inline FloatRoundedRect::operator SkRRect() const {
+diff --git a/third_party/blink/renderer/platform/geometry/path.cc b/third_party/blink/renderer/platform/geometry/path.cc
+index 96a1f38db9f42e8d47d70b82b6e1ee8034b96b47..08bc720fe0337f947461d74e29f7ce6f35509643 100644
+--- a/third_party/blink/renderer/platform/geometry/path.cc
++++ b/third_party/blink/renderer/platform/geometry/path.cc
+@@ -33,6 +33,7 @@
+ 
+ #include <algorithm>
+ 
++#include "electron/shell/renderer/electron_smooth_round_rect.h"
+ #include "third_party/blink/renderer/platform/geometry/skia_geometry_utils.h"
+ #include "third_party/blink/renderer/platform/geometry/stroke_data.h"
+ #include "third_party/blink/renderer/platform/transforms/affine_transform.h"
+@@ -657,6 +658,18 @@ void Path::AddRoundedRect(const FloatRoundedRect& rect, bool clockwise) {
+   // no canvas API (at this moment) to change corner curvature.
+   DCHECK(clockwise);
+ 
++  // TODO(clavin): decompose `electron::DrawSmoothRoundRect` into corners
++  if (rect.IsSmooth()) {
++    const gfx::RectF& box = rect.Rect();
++    const FloatRoundedRect::Radii& radii = rect.GetRadii();
++    path_.addPath(electron::DrawSmoothRoundRect(
++        box.x(), box.y(), box.width(), box.height(),
++        std::min(rect.GetSmoothness(), 1.0f), radii.TopLeft().width(),
++        radii.TopRight().width(), radii.BottomRight().width(),
++        radii.BottomLeft().width()));
++    return;
++  }
++
+   path_.moveTo(gfx::PointFToSkPoint(rect.TopLeftCorner().top_right()));
+ 
+   path_.lineTo(gfx::PointFToSkPoint((rect.TopRightCorner().origin())));
+diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.cc b/third_party/blink/renderer/platform/graphics/graphics_context.cc
+index 5d7887ae589af529a9f4c3e29b9fa314903e26bb..0d2f753a3b189bae69729b4da3cc2af72a98453a 100644
+--- a/third_party/blink/renderer/platform/graphics/graphics_context.cc
++++ b/third_party/blink/renderer/platform/graphics/graphics_context.cc
+@@ -819,6 +819,13 @@ void GraphicsContext::FillRoundedRect(const FloatRoundedRect& rrect,
+     return;
+   }
+ 
++  if (!rrect.HasSimpleRoundedCurvature()) {
++    Path path;
++    path.AddRoundedRect(rrect);
++    FillPath(path, auto_dark_mode);
++    return;
++  }
++
+   const cc::PaintFlags& fill_flags = ImmutableState()->FillFlags();
+   const SkColor4f sk_color = color.toSkColor4f();
+   if (sk_color == fill_flags.getColor4f()) {

+ 5 - 0
shell/browser/web_contents_preferences.cc

@@ -149,6 +149,7 @@ void WebContentsPreferences::Clear() {
   preload_path_ = std::nullopt;
   v8_cache_options_ = blink::mojom::V8CacheOptions::kDefault;
   deprecated_paste_enabled_ = false;
+  corner_smoothing_css_ = true;
 
 #if BUILDFLAG(IS_MAC)
   scroll_bounce_ = false;
@@ -228,6 +229,8 @@ void WebContentsPreferences::SetFromDictionary(
   if (web_preferences.Get(options::kDisableBlinkFeatures,
                           &disable_blink_features))
     disable_blink_features_ = disable_blink_features;
+  web_preferences.Get(options::kEnableCornerSmoothingCSS,
+                      &corner_smoothing_css_);
 
   base::FilePath::StringType preload_path;
   if (web_preferences.Get(options::kPreloadScript, &preload_path)) {
@@ -478,6 +481,8 @@ void WebContentsPreferences::OverrideWebkitPrefs(
   prefs->v8_cache_options = v8_cache_options_;
 
   prefs->dom_paste_enabled = deprecated_paste_enabled_;
+
+  renderer_prefs->electron_corner_smoothing_css = corner_smoothing_css_;
 }
 
 WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPreferences);

+ 1 - 0
shell/browser/web_contents_preferences.h

@@ -134,6 +134,7 @@ class WebContentsPreferences
   std::optional<base::FilePath> preload_path_;
   blink::mojom::V8CacheOptions v8_cache_options_;
   bool deprecated_paste_enabled_ = false;
+  bool corner_smoothing_css_;
 
 #if BUILDFLAG(IS_MAC)
   bool scroll_bounce_;

+ 4 - 0
shell/common/options_switches.h

@@ -210,6 +210,10 @@ inline constexpr std::string_view kSpellcheck = "spellcheck";
 // document.execCommand("paste").
 inline constexpr std::string_view kEnableDeprecatedPaste =
     "enableDeprecatedPaste";
+
+// Whether the -electron-corner-smoothing CSS rule is enabled.
+inline constexpr std::string_view kEnableCornerSmoothingCSS =
+    "enableCornerSmoothingCSS";
 }  // namespace options
 
 // Following are actually command line switches, should be moved to other files.

+ 299 - 0
shell/renderer/electron_smooth_round_rect.cc

@@ -0,0 +1,299 @@
+// Copyright (c) 2024 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "electron/shell/renderer/electron_smooth_round_rect.h"
+
+#include <numbers>
+#include "base/check.h"
+
+namespace electron {
+
+namespace {
+
+// Applies quarter rotations (n * 90°) to a point relative to the origin.
+constexpr SkPoint QuarterRotate(const SkPoint& p,
+                                unsigned int quarter_rotations) {
+  switch (quarter_rotations % 4) {
+    case 0:
+      return p;
+    case 1:
+      return {-p.y(), p.x()};
+    case 2:
+      return {-p.x(), -p.y()};
+    // case 3:
+    default:
+      return {p.y(), -p.x()};
+  }
+}
+
+// Edge length consumed for a given smoothness and corner radius.
+constexpr float LengthForCornerSmoothness(float smoothness, float radius) {
+  return (1.0f + smoothness) * radius;
+}
+
+// The smoothness value when consuming an edge length for a corner with a given
+// radius.
+//
+// This function complements `LengthForCornerSmoothness`:
+//   SmoothnessForCornerLength(LengthForCornerSmoothness(s, r), r) = s
+constexpr float SmoothnessForCornerLength(float length, float radius) {
+  return (length / radius) - 1.0f;
+}
+
+// Geometric measurements for constructing the curves of smooth round corners on
+// a rectangle.
+//
+// Each measurement's value is relative to the rectangle's natural corner point.
+// An "offset" measurement is a one-dimensional length and a "vector"
+// measurement is a two-dimensional pair of lengths.
+//
+// Each measurement's direction is relative to the direction of an edge towards
+// the corner. Offsets are in the same direction as the edge toward the corner.
+// For vectors, the X direction is parallel and the Y direction is
+// perpendicular.
+struct CurveGeometry {
+  constexpr CurveGeometry(float radius, float smoothness);
+
+  constexpr SkVector edge_connecting_vector() const {
+    return {edge_connecting_offset, 0.0f};
+  }
+  constexpr SkVector edge_curve_vector() const {
+    return {edge_curve_offset, 0.0f};
+  }
+  constexpr SkVector arc_curve_vector() const {
+    return {arc_curve_offset, 0.0f};
+  }
+  constexpr SkVector arc_connecting_vector_transposed() const {
+    return {arc_connecting_vector.y(), arc_connecting_vector.x()};
+  }
+
+  // The point where the edge connects to the curve.
+  float edge_connecting_offset;
+
+  // The control point for the curvature where the edge connects to the curve.
+  float edge_curve_offset;
+
+  // The control point for the curvature where the arc connects to the curve.
+  float arc_curve_offset;
+
+  // The point where the arc connects to the curve.
+  SkVector arc_connecting_vector;
+};
+
+constexpr CurveGeometry::CurveGeometry(float radius, float smoothness) {
+  edge_connecting_offset = LengthForCornerSmoothness(smoothness, radius);
+
+  float arc_angle = (std::numbers::pi / 4.0f) * smoothness;
+
+  arc_connecting_vector =
+      SkVector::Make(1.0f - std::sin(arc_angle), 1.0f - std::cos(arc_angle)) *
+      radius;
+
+  arc_curve_offset = (1.0f - std::tan(arc_angle / 2.0f)) * radius;
+
+  constexpr float EDGE_CURVE_POINT_RATIO = 2.0f / 3.0f;
+  edge_curve_offset =
+      edge_connecting_offset -
+      ((edge_connecting_offset - arc_curve_offset) * EDGE_CURVE_POINT_RATIO);
+}
+
+void DrawCorner(SkPath& path,
+                float radius,
+                const CurveGeometry& curve1,
+                const CurveGeometry& curve2,
+                const SkPoint& corner,
+                unsigned int quarter_rotations) {
+  // Move/Line to the edge connecting point
+  {
+    SkPoint edge_connecting_point =
+        corner +
+        QuarterRotate(curve1.edge_connecting_vector(), quarter_rotations + 1);
+
+    if (quarter_rotations == 0) {
+      path.moveTo(edge_connecting_point);
+    } else {
+      path.lineTo(edge_connecting_point);
+    }
+  }
+
+  // Draw the first smoothing curve
+  {
+    SkPoint edge_curve_point =
+        corner +
+        QuarterRotate(curve1.edge_curve_vector(), quarter_rotations + 1);
+    SkPoint arc_curve_point = corner + QuarterRotate(curve1.arc_curve_vector(),
+                                                     quarter_rotations + 1);
+    SkPoint arc_connecting_point =
+        corner + QuarterRotate(curve1.arc_connecting_vector_transposed(),
+                               quarter_rotations);
+    path.cubicTo(edge_curve_point, arc_curve_point, arc_connecting_point);
+  }
+
+  // Draw the arc
+  {
+    SkPoint arc_connecting_point =
+        corner + QuarterRotate(curve2.arc_connecting_vector, quarter_rotations);
+    path.arcTo(SkPoint::Make(radius, radius), 0.0f, SkPath::kSmall_ArcSize,
+               SkPathDirection::kCW, arc_connecting_point);
+  }
+
+  // Draw the second smoothing curve
+  {
+    SkPoint arc_curve_point =
+        corner + QuarterRotate(curve2.arc_curve_vector(), quarter_rotations);
+    SkPoint edge_curve_point =
+        corner + QuarterRotate(curve2.edge_curve_vector(), quarter_rotations);
+    SkPoint edge_connecting_point =
+        corner +
+        QuarterRotate(curve2.edge_connecting_vector(), quarter_rotations);
+    path.cubicTo(arc_curve_point, edge_curve_point, edge_connecting_point);
+  }
+}
+
+// Constrains the smoothness of two corners along the same edge.
+//
+// If the smoothness value needs to be constrained, it will try to keep the
+// ratio of the smoothness values the same as the ratio of the radii
+// (`s1/s2 = r1/r2`).
+constexpr std::pair<float, float> ConstrainSmoothness(float size,
+                                                      float smoothness,
+                                                      float radius1,
+                                                      float radius2) {
+  float edge_consumed1 = LengthForCornerSmoothness(smoothness, radius1);
+  float edge_consumed2 = LengthForCornerSmoothness(smoothness, radius2);
+
+  // If both corners fit within the edge size then keep the smoothness
+  if (edge_consumed1 + edge_consumed2 <= size) {
+    return {smoothness, smoothness};
+  }
+
+  float ratio = radius1 / (radius1 + radius2);
+  float length1 = size * ratio;
+  float length2 = size - length1;
+
+  float smoothness1 =
+      std::max(SmoothnessForCornerLength(length1, radius1), 0.0f);
+  float smoothness2 =
+      std::max(SmoothnessForCornerLength(length2, radius2), 0.0f);
+
+  return {smoothness1, smoothness2};
+}
+
+}  // namespace
+
+// The algorithm for drawing this shape is based on the article
+// "Desperately seeking squircles" by Daniel Furse. A brief summary:
+//
+// In a simple round rectangle, each corner of a plain rectangle is replaced
+// with a quarter circle and connected to each edge of the corner.
+//
+//        Edge
+//      ←------→       ↖
+//     ----------o--__  `、 Corner (Quarter Circle)
+//                    `、 `、
+//                      |   ↘
+//                       |
+//                       o
+//                       | ↑
+//                       | | Edge
+//                       | ↓
+//
+// This creates sharp changes in the curvature at the points where the edge
+// transitions to the corner, suddenly curving at a constant rate. Our primary
+// goal is to smooth out that curvature profile, slowly ramping up and back
+// down, like turning a car with the steering wheel.
+//
+// To achieve this, we "expand" that point where the circular corner meets the
+// straight edge in both directions. We use this extra space to construct a
+// small curved path that eases the curvature from the edge to the corner
+// circle.
+//
+//      Edge  Curve
+//      ←--→ ←-----→
+//     -----o----___o   ↖、 Corner (Circular Arc)
+//                    `、 `↘
+//                      o
+//                      |  ↑
+//                       | | Curve
+//                       | ↓
+//                       o
+//                       | ↕ Edge
+//
+// Each curve is implemented as a cubic Bézier curve, composed of four control
+// points:
+//
+// * The first control point connects to the straight edge.
+// * The fourth (last) control point connects to the circular arc.
+// * The second & third control points both lie on the infinite line extending
+//   from the straight edge.
+// * The third control point (only) also lies on the infinite line tangent to
+//   the circular arc at the fourth control point.
+//
+// The first and fourth (last) control points are firmly fixed by attaching to
+// the straight edge and circular arc, respectively. The third control point is
+// fixed at the intersection between the edge and tangent lines. The second
+// control point, however, is only constrained to the infinite edge line, but
+// we may choose where.
+SkPath DrawSmoothRoundRect(float x,
+                           float y,
+                           float width,
+                           float height,
+                           float smoothness,
+                           float top_left_radius,
+                           float top_right_radius,
+                           float bottom_right_radius,
+                           float bottom_left_radius) {
+  DCHECK(0.0f <= smoothness && smoothness <= 1.0f);
+
+  // Assume the radii are already constrained within the rectangle size
+  DCHECK(top_left_radius + top_right_radius <= width);
+  DCHECK(bottom_left_radius + bottom_right_radius <= width);
+  DCHECK(top_left_radius + bottom_left_radius <= height);
+  DCHECK(top_right_radius + bottom_right_radius <= height);
+
+  if (width <= 0.0f || height <= 0.0f) {
+    return SkPath();
+  }
+
+  // Constrain the smoothness for each curve on each edge
+  auto [top_left_smoothness, top_right_smoothness] =
+      ConstrainSmoothness(width, smoothness, top_left_radius, top_right_radius);
+  auto [right_top_smoothness, right_bottom_smoothness] = ConstrainSmoothness(
+      height, smoothness, top_right_radius, bottom_right_radius);
+  auto [bottom_left_smoothness, bottom_right_smoothness] = ConstrainSmoothness(
+      width, smoothness, bottom_left_radius, bottom_right_radius);
+  auto [left_top_smoothness, left_bottom_smoothness] = ConstrainSmoothness(
+      height, smoothness, top_left_radius, bottom_left_radius);
+
+  SkPath path;
+
+  // Top left corner
+  DrawCorner(path, top_left_radius,
+             CurveGeometry(top_left_radius, left_top_smoothness),
+             CurveGeometry(top_left_radius, 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),
+             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);
+
+  // 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);
+
+  path.close();
+  return path;
+}
+
+}  // namespace electron

+ 36 - 0
shell/renderer/electron_smooth_round_rect.h

@@ -0,0 +1,36 @@
+// Copyright (c) 2024 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_RENDERER_API_ELECTRON_SMOOTH_ROUND_RECT_H_
+#define ELECTRON_SHELL_RENDERER_API_ELECTRON_SMOOTH_ROUND_RECT_H_
+
+#include "third_party/skia/include/core/SkPath.h"
+
+namespace electron {
+
+// Draws a rectangle that has smooth round corners for a given "smoothness"
+// value between 0.0 and 1.0 (representing 0% to 100%).
+//
+// The smoothness value determines how much edge length can be consumed by each
+// corner, scaling with respect to that corner's radius. The smoothness will
+// dynamically scale back if there is not enough edge length, similar to how
+// the corner radius backs off when there isn't enough edge length.
+//
+// Each corner's radius can be supplied independently. Corner radii are expected
+// to already be balanced (Radius1 + Radius2 <= Length, for each given side).
+//
+// Elliptical corner radii are not currently supported.
+SkPath DrawSmoothRoundRect(float x,
+                           float y,
+                           float width,
+                           float height,
+                           float smoothness,
+                           float top_left_radius,
+                           float top_right_radius,
+                           float bottom_right_radius,
+                           float bottom_left_radius);
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_RENDERER_API_ELECTRON_SMOOTH_ROUND_RECT_H_

+ 156 - 0
spec/api-corner-smoothing-spec.ts

@@ -0,0 +1,156 @@
+import { NativeImage, nativeImage } from 'electron/common';
+import { BrowserWindow } from 'electron/main';
+
+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(
+  __dirname,
+  'fixtures',
+  'api',
+  'corner-smoothing'
+);
+
+/**
+ * Rendered images may "match" but slightly differ due to rendering artifacts
+ * like anti-aliasing and vector path resolution, among others. This tolerance
+ * is the cutoff for whether two images "match" or not.
+ *
+ * From testing, matching images were found to have an average global difference
+ * up to about 1.3 and mismatched images were found to have a difference of at
+ * least about 7.3.
+ *
+ * See the documentation on `compareImages` for more information.
+ */
+const COMPARISON_TOLERANCE = 2.5;
+
+/**
+ * Compares the average global difference of two images to determine if they
+ * are similar enough to "match" each other.
+ *
+ * "Average global difference" is the average difference of pixel components
+ * (RGB each) across an entire image.
+ *
+ * The cutoff for matching/not-matching is defined by the `COMPARISON_TOLERANCE`
+ * constant.
+ */
+function compareImages (img1: NativeImage, img2: NativeImage): boolean {
+  expect(img1.getSize()).to.deep.equal(
+    img2.getSize(),
+    'Cannot compare images with different sizes'
+  );
+
+  const bitmap1 = img1.toBitmap();
+  const bitmap2 = img2.toBitmap();
+
+  const { width, height } = img1.getSize();
+
+  let totalDiff = 0;
+  for (let x = 0; x < width; x++) {
+    for (let y = 0; y < height; y++) {
+      const index = (x + y * width) * 4;
+      const pixel1 = bitmap1.subarray(index, index + 4);
+      const pixel2 = bitmap2.subarray(index, index + 4);
+      const diff =
+        Math.abs(pixel1[0] - pixel2[0]) +
+        Math.abs(pixel1[1] - pixel2[1]) +
+        Math.abs(pixel1[2] - pixel2[2]);
+      totalDiff += diff;
+    }
+  }
+
+  const avgDiff = totalDiff / (width * height);
+  return avgDiff <= COMPARISON_TOLERANCE;
+}
+
+/**
+ * Recipe for tests.
+ *
+ * The page is rendered, captured as an image, then compared to an expected
+ * result image.
+ */
+async function pageCaptureTestRecipe (
+  pagePath: string,
+  expectedImgPath: string,
+  artifactName: string,
+  cornerSmoothingAvailable: boolean = true
+): Promise<void> {
+  const w = new BrowserWindow({
+    show: false,
+    width: 800,
+    height: 600,
+    useContentSize: true,
+    webPreferences: {
+      enableCornerSmoothingCSS: cornerSmoothingAvailable
+    }
+  });
+  await w.loadFile(pagePath);
+  w.show();
+
+  // Wait for a render frame to prepare the page.
+  await w.webContents.executeJavaScript(
+    'new Promise((resolve) => { requestAnimationFrame(() => resolve()); })'
+  );
+
+  const actualImg = await w.webContents.capturePage();
+  expect(actualImg.isEmpty()).to.be.false('Failed to capture page image');
+
+  const expectedImg = nativeImage.createFromPath(expectedImgPath);
+  expect(expectedImg.isEmpty()).to.be.false(
+    'Failed to read expected reference image'
+  );
+
+  const matches = compareImages(actualImg, expectedImg);
+  if (!matches) {
+    const artifactFileName = `corner-rounding-expected-${artifactName}.png`;
+    await createArtifact(artifactFileName, actualImg.toPNG());
+
+    throw new AssertionError(
+      `Actual image did not match expected reference image. Actual: "${artifactFileName}" in artifacts, Expected: "${path.relative(
+        path.resolve(__dirname, '..'),
+        expectedImgPath
+      )}" in source`
+    );
+  }
+}
+
+// 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', () => {
+  afterEach(async () => {
+    await closeAllWindows();
+  });
+
+  describe('shape', () => {
+    for (const available of [true, false]) {
+      it(`matches the reference with web preference = ${available}`, async () => {
+        await pageCaptureTestRecipe(
+          path.join(FIXTURE_PATH, 'shape', 'test.html'),
+          path.join(FIXTURE_PATH, 'shape', `expected-${available}.png`),
+          `shape-${available}`,
+          available
+        );
+      });
+    }
+  });
+
+  describe('system-ui keyword', () => {
+    const { platform } = process;
+    it(`matches the reference for platform = ${platform}`, async () => {
+      await pageCaptureTestRecipe(
+        path.join(FIXTURE_PATH, 'system-ui-keyword', 'test.html'),
+        path.join(
+          FIXTURE_PATH,
+          'system-ui-keyword',
+          `expected-${platform}.png`
+        ),
+        `system-ui-${platform}`
+      );
+    });
+  });
+});

BIN
spec/fixtures/api/corner-smoothing/shape/expected-false.png


BIN
spec/fixtures/api/corner-smoothing/shape/expected-true.png


BIN
spec/fixtures/api/corner-smoothing/shape/image.png


+ 136 - 0
spec/fixtures/api/corner-smoothing/shape/test.html

@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      /* Page is expected to be exactly 800x600 */
+      html, body {
+        width: 800px;
+        height: 600px;
+        margin: 0;
+        overflow: hidden;
+
+        display: flex;
+        flex-flow: column nowrap;
+        justify-content: space-around;
+        align-items: stretch;
+      }
+
+      .row {
+        display: flex;
+        flex-flow: row nowrap;
+        justify-content: space-around;
+        align-items: center;
+      }
+      .row.rounding-0 > .box {
+        -electron-corner-smoothing: 0%;
+      }
+      .row.rounding-30 > .box {
+        -electron-corner-smoothing: 30%;
+      }
+      .row.rounding-60 > .box {
+        -electron-corner-smoothing: 60%;
+      }
+      .row.rounding-100 > .box {
+        -electron-corner-smoothing: 100%;
+      }
+      .row.rounding-invalid > .box {
+        -electron-corner-smoothing: 200%;
+      }
+      .row.rounding-invalid > .box:nth-child(2) {
+        -electron-corner-smoothing: -10%;
+      }
+      .row.rounding-invalid > .box:nth-child(3) {
+        -electron-corner-smoothing: -200%;
+      }
+
+      .box {
+        --boxes-x: 7;
+        --boxes-y: 5;
+        --box-shadow-offset: 4px;
+        --box-shadow-spread: 2px;
+        --box-shadow-grow: 2px;
+
+        --box-gap: calc(var(--box-shadow-offset) + var(--box-shadow-spread) + var(--box-shadow-grow) + 4px);
+        --box-size: min(calc(((100vw - var(--box-gap)) / var(--boxes-x)) - var(--box-gap)), calc(((100vh - var(--box-gap)) / var(--boxes-y)) - var(--box-gap)));
+
+        width: var(--box-size);
+        height: var(--box-size);
+        border-radius: calc((var(--box-size) / 4) - 4px);
+        box-sizing: border-box;
+
+        background-color: black;
+      }
+      .box.outline {
+        background-color: bisque;
+        border: 8px solid black;
+      }
+      .box.outline.dashed {
+        background-color: darkkhaki;
+        border-style: dashed;
+      }
+      .box.outline.double {
+        background-color: darkseagreen;
+        border-style: double;
+      }
+      .box.outer {
+        overflow: clip;
+      }
+      .box.outer > .inner {
+        width: 100%;
+        height: 100%;
+        background-color: blueviolet;
+      }
+      .box.shadow {
+        background-color: skyblue;
+        box-shadow: var(--box-shadow-offset) var(--box-shadow-offset) var(--box-shadow-spread) var(--box-shadow-grow) cornflowerblue;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="row rounding-0">
+      <div class="box"></div>
+      <img class="box" src="image.png" />
+      <div class="box outline"></div>
+      <div class="box outline dashed"></div>
+      <div class="box outline double"></div>
+      <div class="box outer"><div class="inner"></div></div>
+      <div class="box shadow"></div>
+    </div>
+    <div class="row rounding-30">
+      <div class="box"></div>
+      <img class="box" src="image.png" />
+      <div class="box outline"></div>
+      <div class="box outline dashed"></div>
+      <div class="box outline double"></div>
+      <div class="box outer"><div class="inner"></div></div>
+      <div class="box shadow"></div>
+    </div>
+    <div class="row rounding-60">
+      <div class="box"></div>
+      <img class="box" src="image.png" />
+      <div class="box outline"></div>
+      <div class="box outline dashed"></div>
+      <div class="box outline double"></div>
+      <div class="box outer"><div class="inner"></div></div>
+      <div class="box shadow"></div>
+    </div>
+    <div class="row rounding-100">
+      <div class="box"></div>
+      <img class="box" src="image.png" />
+      <div class="box outline"></div>
+      <div class="box outline dashed"></div>
+      <div class="box outline double"></div>
+      <div class="box outer"><div class="inner"></div></div>
+      <div class="box shadow"></div>
+    </div>
+    <div class="row rounding-invalid">
+      <div class="box"></div>
+      <img class="box" src="image.png" />
+      <div class="box outline"></div>
+      <div class="box outline dashed"></div>
+      <div class="box outline double"></div>
+      <div class="box outer"><div class="inner"></div></div>
+      <div class="box shadow"></div>
+    </div>
+  </body>
+</html>

BIN
spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-darwin.png


BIN
spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-linux.png


BIN
spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-win32.png


+ 42 - 0
spec/fixtures/api/corner-smoothing/system-ui-keyword/test.html

@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      /* Page is expected to be exactly 800x600 */
+      html, body {
+        width: 800px;
+        height: 600px;
+        margin: 0;
+        overflow: hidden;
+
+        display: flex;
+        flex-flow: row nowrap;
+        justify-content: space-around;
+        align-items: center;
+      }
+
+      .box {
+        width: 256px;
+        height: 256px;
+        border-radius: 48px;
+
+        background-color: cornflowerblue;
+      }
+
+      .box.rounding-0 {
+        -electron-corner-smoothing: 0%;
+      }
+      .box.rounding-system-ui {
+        -electron-corner-smoothing: system-ui;
+      }
+      .box.rounding-100 {
+        -electron-corner-smoothing: 100%;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="box rounding-0"></div>
+    <div class="box rounding-system-ui"></div>
+    <div class="box rounding-100"></div>
+  </body>
+</html>