feat: Corner Smoothing CSS rule (Reland) (#46385)

* feat: Corner Smoothing CSS rule (Reland)

Reland of #45185

Co-authored-by: Calvin <clavin@users.noreply.github.com>

* Fix patch conflicts

Co-authored-by: clavin <clavin@electronjs.org>

* fixup! Fix patch conflicts

Co-authored-by: clavin <clavin@electronjs.org>

* Update expected image

The dashed border is subtly different. The new version is correct and the old one was incorrect.

Co-authored-by: clavin <clavin@electronjs.org>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Calvin <clavin@users.noreply.github.com>
Co-authored-by: clavin <clavin@electronjs.org>
This commit is contained in:
trop[bot] 2025-04-01 08:49:44 -05:00 committed by GitHub
parent 71e53c925e
commit 75e44e5f05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1276 additions and 1 deletions

View file

@ -96,8 +96,9 @@ These individual tutorials expand on topics discussed in the guide above.
* [Chrome Extensions Support](api/extensions.md) * [Chrome Extensions Support](api/extensions.md)
* [Breaking API Changes](breaking-changes.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) * [`<webview>` Tag](api/webview-tag.md)
* [`window.open` Function](api/window-open.md) * [`window.open` Function](api/window-open.md)

View file

@ -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
```

View file

@ -149,6 +149,7 @@
`WebContents` when the preferred size changes. Default is `false`. `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. * `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`. * `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 [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 [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5

View file

@ -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>

After

Width:  |  Height:  |  Size: 265 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 405 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 623 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 631 B

View file

@ -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>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

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

View file

@ -718,6 +718,8 @@ filenames = {
"shell/renderer/electron_renderer_client.h", "shell/renderer/electron_renderer_client.h",
"shell/renderer/electron_sandboxed_renderer_client.cc", "shell/renderer/electron_sandboxed_renderer_client.cc",
"shell/renderer/electron_sandboxed_renderer_client.h", "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.cc",
"shell/renderer/preload_realm_context.h", "shell/renderer/preload_realm_context.h",
"shell/renderer/preload_utils.cc", "shell/renderer/preload_utils.cc",

View file

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

View file

@ -0,0 +1,485 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Calvin Watford <watfordcalvin@gmail.com>
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::ContouredRect` to add smoothness.
* Modifies graphics to handle smooth `ContouredRect`s, delegating to
`//electron/shell/renderer/electron_smooth_round_rect`.
3. Adds a renderer preference / web setting:
* Controls whether the CSS rule is available.
* Mostly simple "plumbing" for the setting through blink.
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 3e3d56992ab135ee88257681f93e39a470192857..26e87c2be381c0fd7d5116d95a107082e2549eae 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 a53b4901dde0dc83dce6c9b56616eef0d02d94a5..b419672af985f673f375fbb63b4d2b2c419e3e03 100644
--- a/third_party/blink/public/web/web_settings.h
+++ b/third_party/blink/public/web/web_settings.h
@@ -285,6 +285,7 @@ class WebSettings {
virtual void SetRequireTransientActivationAndAuthorizationForSubAppsAPIs(
bool) = 0;
virtual void SetRootScrollbarThemeColor(std::optional<SkColor>) = 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 6cf39b4a15ac290891d56a8d1d7b30846a329f79..5a0d840d5c01fb1ed95bacd36cc4f01443afdf94 100644
--- a/third_party/blink/renderer/core/css/css_properties.json5
+++ b/third_party/blink/renderer/core/css/css_properties.json5
@@ -8724,6 +8724,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 998fb2cfb682e61d89bb6f832cd91efa658f9773..8ad689bd9327569d26eb5f449a707d6b0d7c2536 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 c1aa3851a8530f1993de160773d3fae107c4d8bd..d0b87808b0d0466473d21720e44366228daed218 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
@@ -11857,5 +11857,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 797c8e2d7ee777bcd88e0e4e6a65992342c2a098..c8d024213eb4dfe1ae82e0543f066df55555213e 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
@@ -3861,4 +3861,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 4a29a2200eaab5084078e928a68c862296c6ff91..fcd879deec0e68b3b6988402d19570cf0065daa2 100644
--- a/third_party/blink/renderer/core/exported/web_settings_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_settings_impl.cc
@@ -816,4 +816,8 @@ void WebSettingsImpl::SetRootScrollbarThemeColor(
settings_->SetRootScrollbarThemeColor(theme_color);
}
+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 eabcddfa5f17497ef0611fa43f77dd13e2a54e00..96266c4f8c17b589f3d9c549e2836a147b7401ce 100644
--- a/third_party/blink/renderer/core/exported/web_settings_impl.h
+++ b/third_party/blink/renderer/core/exported/web_settings_impl.h
@@ -237,6 +237,7 @@ class CORE_EXPORT WebSettingsImpl final : public WebSettings {
void SetRequireTransientActivationAndAuthorizationForSubAppsAPIs(
bool) override;
void SetRootScrollbarThemeColor(std::optional<SkColor>) 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 9810991e0a5d8b931a70e056b6651b8e5fdb9881..2b37a0209d370629f08e9065a22b92ff52053141 100644
--- a/third_party/blink/renderer/core/exported/web_view_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_view_impl.cc
@@ -3574,6 +3574,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 f4cdee12ea4352067f5de3e074e43d51ef56d2e5..6377e4b1ea8aa46b0bf69f8420b6c439bea70dba 100644
--- a/third_party/blink/renderer/core/frame/settings.json5
+++ b/third_party/blink/renderer/core/frame/settings.json5
@@ -1261,5 +1261,10 @@
initial: false,
type: "bool"
},
+ {
+ name: "electronCornerSmoothingCSS",
+ initial: true,
+ invalidate: ["Style"],
+ },
],
}
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 68dbf4accafc0ce8100d6d488195e9dcde8b1502..dcd13e67acde42b181b219b2f690e2fc76ad917d 100644
--- a/third_party/blink/renderer/core/paint/box_painter_base.cc
+++ b/third_party/blink/renderer/core/paint/box_painter_base.cc
@@ -324,8 +324,9 @@ void BoxPainterBase::PaintNormalBoxShadow(const PaintInfo& info,
if (has_border_radius) {
FloatRoundedRect rounded_fill_rect(fill_rect, border.GetRadii());
ApplySpreadToShadowShape(rounded_fill_rect, shadow.Spread());
- context.FillRoundedRect(
- rounded_fill_rect, Color::kBlack,
+ ContouredRect contoured_fill_rect(rounded_fill_rect, border.GetCornerCurvature());
+ context.FillContouredRect(
+ contoured_fill_rect, Color::kBlack,
PaintAutoDarkMode(style, DarkModeFilter::ElementRole::kBackground));
} else {
fill_rect.Outset(shadow.Spread());
@@ -413,16 +414,20 @@ void BoxPainterBase::PaintInsetBoxShadow(const PaintInfo& info,
AdjustRectForSideClipping(inner_rect, shadow, sides_to_include);
FloatRoundedRect inner_rounded_rect(inner_rect, bounds.GetRadii());
ApplySpreadToShadowShape(inner_rounded_rect, -shadow.Spread());
+ ContouredRect contoured_bounds(
+ bounds, ContouredBorderGeometry::ContouredBorder(
+ style, PhysicalRect::EnclosingRect(bounds.Rect()))
+ .GetCornerCurvature());
if (inner_rounded_rect.IsEmpty()) {
// |AutoDarkMode::Disabled()| is used because |shadow_color| has already
// been adjusted for dark mode.
- context.FillRoundedRect(bounds, shadow_color, AutoDarkMode::Disabled());
+ context.FillContouredRect(contoured_bounds, shadow_color, AutoDarkMode::Disabled());
continue;
}
GraphicsContextStateSaver state_saver(context);
if (bounds.IsRounded()) {
// TODO(crbug.com/397459628) render corner-shape with box-shadow
- context.ClipContouredRect(ContouredRect(bounds));
+ context.ClipContouredRect(contoured_bounds);
} else {
context.Clip(bounds.Rect());
}
diff --git a/third_party/blink/renderer/core/paint/contoured_border_geometry.cc b/third_party/blink/renderer/core/paint/contoured_border_geometry.cc
index b96a3ba1e16b15807086c8e6a256b256b48e8adb..1396fd3214e18e1ded8fd8a83d964c8c824fbc5e 100644
--- a/third_party/blink/renderer/core/paint/contoured_border_geometry.cc
+++ b/third_party/blink/renderer/core/paint/contoured_border_geometry.cc
@@ -43,6 +43,24 @@ float EffectiveCurvature(Superellipse superellipse, const gfx::SizeF& radius) {
: superellipse.Exponent();
}
+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;
+}
+
ContouredRect::CornerCurvature CalcCurvatureFor(
const ComputedStyle& style,
const FloatRoundedRect::Radii& radii) {
@@ -50,7 +68,8 @@ ContouredRect::CornerCurvature CalcCurvatureFor(
EffectiveCurvature(style.CornerTopLeftShape(), radii.TopLeft()),
EffectiveCurvature(style.CornerTopRightShape(), radii.TopRight()),
EffectiveCurvature(style.CornerBottomRightShape(), radii.BottomRight()),
- EffectiveCurvature(style.CornerBottomLeftShape(), radii.BottomLeft()));
+ EffectiveCurvature(style.CornerBottomLeftShape(), radii.BottomLeft()),
+ SmoothnessFromLength(style.ElectronCornerSmoothing()));
}
ContouredRect PixelSnappedContouredBorderInternal(
diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
index ae923ecf6c58d608ed3583312edb48ecb6b7f576..759d991162702d31e77590474bc5764c8fd8229b 100644
--- a/third_party/blink/renderer/platform/BUILD.gn
+++ b/third_party/blink/renderer/platform/BUILD.gn
@@ -1642,6 +1642,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/contoured_rect.h b/third_party/blink/renderer/platform/geometry/contoured_rect.h
index 1a5d76b145307c11ac71cd5840f7ca166655fde2..a40aee431d9ecf8bdb011ccc4e8a9ecd813ffe3a 100644
--- a/third_party/blink/renderer/platform/geometry/contoured_rect.h
+++ b/third_party/blink/renderer/platform/geometry/contoured_rect.h
@@ -42,19 +42,29 @@ class PLATFORM_EXPORT ContouredRect {
constexpr CornerCurvature(float top_left,
float top_right,
float bottom_right,
- float bottom_left)
+ float bottom_left,
+ float smoothness)
: top_left_(top_left),
top_right_(top_right),
bottom_right_(bottom_right),
- bottom_left_(bottom_left) {
+ bottom_left_(bottom_left),
+ smoothness_(smoothness) {
DCHECK_GE(top_left, 0);
DCHECK_GE(top_right, 0);
DCHECK_GE(bottom_right, 0);
DCHECK_GE(bottom_left, 0);
+ DCHECK_GE(smoothness, 0);
}
+ constexpr CornerCurvature(float top_left,
+ float top_right,
+ float bottom_right,
+ float bottom_left)
+ : CornerCurvature(top_left, top_right, bottom_right, bottom_left, 0) {}
+
+ constexpr bool IsSmooth() const { return smoothness_ > 0.0f; }
constexpr bool IsRound() const {
- return (top_left_ == kRound) && IsUniform();
+ return (top_left_ == kRound) && IsUniform() && !IsSmooth();
}
constexpr bool IsUniform() const {
@@ -66,6 +76,7 @@ class PLATFORM_EXPORT ContouredRect {
constexpr float TopRight() const { return top_right_; }
constexpr float BottomRight() const { return bottom_right_; }
constexpr float BottomLeft() const { return bottom_left_; }
+ constexpr float Smoothness() const { return smoothness_; }
constexpr bool operator==(const CornerCurvature&) const = default;
@@ -76,6 +87,7 @@ class PLATFORM_EXPORT ContouredRect {
float top_right_ = kRound;
float bottom_right_ = kRound;
float bottom_left_ = kRound;
+ float smoothness_ = 0.0f;
};
constexpr ContouredRect() = default;
diff --git a/third_party/blink/renderer/platform/geometry/path.cc b/third_party/blink/renderer/platform/geometry/path.cc
index 4b63f7f3e113e77bf91810b91c5fad1b6bf5de92..6121bd490717ce6bf4ba7d933e1a9f3eae1752e1 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/contoured_rect.h"
#include "third_party/blink/renderer/platform/geometry/float_rounded_rect.h"
#include "third_party/blink/renderer/platform/geometry/skia_geometry_utils.h"
@@ -660,6 +661,18 @@ void Path::AddContouredRect(const ContouredRect& contoured_rect) {
return;
}
+ // TODO(clavin): decompose `electron::DrawSmoothRoundRect` into corners
+ if (contoured_rect.GetCornerCurvature().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(contoured_rect.GetCornerCurvature().Smoothness(), 1.0f),
+ radii.TopLeft().width(), radii.TopRight().width(),
+ radii.BottomRight().width(), radii.BottomLeft().width()));
+ return;
+ }
+
const ContouredRect::CornerCurvature& curvature =
contoured_rect.GetCornerCurvature();
path_.moveTo(gfx::PointFToSkPoint(rect.TopLeftCorner().top_right()));
diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.cc b/third_party/blink/renderer/platform/graphics/graphics_context.cc
index 6361cc655af8c2bef6803efe6f3c382c1eadb851..9439df63a7d265d1f93c89c275d84a8a1dde30c6 100644
--- a/third_party/blink/renderer/platform/graphics/graphics_context.cc
+++ b/third_party/blink/renderer/platform/graphics/graphics_context.cc
@@ -924,6 +924,19 @@ void GraphicsContext::FillRectWithRoundedHole(
DarkModeFlags(this, auto_dark_mode, flags));
}
+void GraphicsContext::FillContouredRect(const ContouredRect& contoured_rect,
+ const Color& color,
+ const AutoDarkMode& auto_dark_mode) {
+ if (contoured_rect.HasRoundCurvature()) {
+ FillRoundedRect(contoured_rect.AsRoundedRect(), color, auto_dark_mode);
+ return;
+ }
+
+ cc::PaintFlags flags = ImmutableState()->FillFlags();
+ flags.setColor(color.toSkColor4f());
+ canvas_->drawPath(contoured_rect.GetPath().GetSkPath(), flags);
+}
+
void GraphicsContext::FillEllipse(const gfx::RectF& ellipse,
const AutoDarkMode& auto_dark_mode) {
DrawOval(gfx::RectFToSkRect(ellipse), ImmutableState()->FillFlags(),
diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.h b/third_party/blink/renderer/platform/graphics/graphics_context.h
index 632b0ec1faebc87d13a5538812333bf14f9e402a..ee51cb455600f507e3a97fe3e6f293ff0f47bbd6 100644
--- a/third_party/blink/renderer/platform/graphics/graphics_context.h
+++ b/third_party/blink/renderer/platform/graphics/graphics_context.h
@@ -318,6 +318,9 @@ class PLATFORM_EXPORT GraphicsContext {
const FloatRoundedRect& rounded_hole_rect,
const Color&,
const AutoDarkMode& auto_dark_mode);
+ void FillContouredRect(const ContouredRect& contoured_rect,
+ const Color& color,
+ const AutoDarkMode& auto_dark_mode);
void StrokeRect(const gfx::RectF&,
const AutoDarkMode& auto_dark_mode);

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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_

View file

@ -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}`
);
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -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>