diff --git a/docs/README.md b/docs/README.md index 98ae1ad9a79a..01663cc01776 100644 --- a/docs/README.md +++ b/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) * [`` Tag](api/webview-tag.md) * [`window.open` Function](api/window-open.md) diff --git a/docs/api/corner-smoothing-css.md b/docs/api/corner-smoothing-css.md new file mode 100644 index 000000000000..029e74657ff3 --- /dev/null +++ b/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 = + | + system-ui +``` diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index 26ead57c2329..4e6710523615 100644 --- a/docs/api/structures/web-preferences.md +++ b/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 diff --git a/docs/images/corner-smoothing-example-0.svg b/docs/images/corner-smoothing-example-0.svg new file mode 100644 index 000000000000..928435ed0b00 --- /dev/null +++ b/docs/images/corner-smoothing-example-0.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/corner-smoothing-example-100.svg b/docs/images/corner-smoothing-example-100.svg new file mode 100644 index 000000000000..34d0802239be --- /dev/null +++ b/docs/images/corner-smoothing-example-100.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/corner-smoothing-example-30.svg b/docs/images/corner-smoothing-example-30.svg new file mode 100644 index 000000000000..4996a25253ce --- /dev/null +++ b/docs/images/corner-smoothing-example-30.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/corner-smoothing-example-60.svg b/docs/images/corner-smoothing-example-60.svg new file mode 100644 index 000000000000..cb1e68a8ff68 --- /dev/null +++ b/docs/images/corner-smoothing-example-60.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/corner-smoothing-summary.svg b/docs/images/corner-smoothing-summary.svg new file mode 100644 index 000000000000..75bffa8e259d --- /dev/null +++ b/docs/images/corner-smoothing-summary.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/filenames.auto.gni b/filenames.auto.gni index a2514d5e9967..df963e2c831b 100644 --- a/filenames.auto.gni +++ b/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", diff --git a/filenames.gni b/filenames.gni index 0d58b9e147ff..ea1637c99030 100644 --- a/filenames.gni +++ b/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", diff --git a/patches/chromium/.patches b/patches/chromium/.patches index cb7cb32aa758..c6e8b731e2be 100644 --- a/patches/chromium/.patches +++ b/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_resolveshortcutproperties.patch diff --git a/patches/chromium/feat_corner_smoothing_css_rule_and_blink_painting.patch b/patches/chromium/feat_corner_smoothing_css_rule_and_blink_painting.patch new file mode 100644 index 000000000000..0e5238145797 --- /dev/null +++ b/patches/chromium/feat_corner_smoothing_css_rule_and_blink_painting.patch @@ -0,0 +1,485 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Calvin Watford +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 StructTraitselectron_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 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 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) = 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(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(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 +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) 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 + ++#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); diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index 5066bc2e00d2..7096c8bc5f81 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/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); diff --git a/shell/browser/web_contents_preferences.h b/shell/browser/web_contents_preferences.h index 4bb6132752cc..1e0b47631f47 100644 --- a/shell/browser/web_contents_preferences.h +++ b/shell/browser/web_contents_preferences.h @@ -134,6 +134,7 @@ class WebContentsPreferences std::optional preload_path_; blink::mojom::V8CacheOptions v8_cache_options_; bool deprecated_paste_enabled_ = false; + bool corner_smoothing_css_; #if BUILDFLAG(IS_MAC) bool scroll_bounce_; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index 7f2608410c4a..6d0895d4cbb3 100644 --- a/shell/common/options_switches.h +++ b/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. diff --git a/shell/renderer/electron_smooth_round_rect.cc b/shell/renderer/electron_smooth_round_rect.cc new file mode 100644 index 000000000000..dcd681387c1f --- /dev/null +++ b/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 +#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 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 diff --git a/shell/renderer/electron_smooth_round_rect.h b/shell/renderer/electron_smooth_round_rect.h new file mode 100644 index 000000000000..1c84da904838 --- /dev/null +++ b/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_ diff --git a/spec/api-corner-smoothing-spec.ts b/spec/api-corner-smoothing-spec.ts new file mode 100644 index 000000000000..a25a787db762 --- /dev/null +++ b/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 { + 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}` + ); + }); + }); +}); diff --git a/spec/fixtures/api/corner-smoothing/shape/expected-false.png b/spec/fixtures/api/corner-smoothing/shape/expected-false.png new file mode 100644 index 000000000000..31aea673f8b0 Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/shape/expected-false.png differ diff --git a/spec/fixtures/api/corner-smoothing/shape/expected-true.png b/spec/fixtures/api/corner-smoothing/shape/expected-true.png new file mode 100644 index 000000000000..f48bc31e2886 Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/shape/expected-true.png differ diff --git a/spec/fixtures/api/corner-smoothing/shape/image.png b/spec/fixtures/api/corner-smoothing/shape/image.png new file mode 100644 index 000000000000..44a04bf32e5e Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/shape/image.png differ diff --git a/spec/fixtures/api/corner-smoothing/shape/test.html b/spec/fixtures/api/corner-smoothing/shape/test.html new file mode 100644 index 000000000000..994e1bd76ee2 --- /dev/null +++ b/spec/fixtures/api/corner-smoothing/shape/test.html @@ -0,0 +1,136 @@ + + + + + + +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-darwin.png b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-darwin.png new file mode 100644 index 000000000000..e1d753c50fa4 Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-darwin.png differ diff --git a/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-linux.png b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-linux.png new file mode 100644 index 000000000000..79341eecfd76 Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-linux.png differ diff --git a/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-win32.png b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-win32.png new file mode 100644 index 000000000000..79341eecfd76 Binary files /dev/null and b/spec/fixtures/api/corner-smoothing/system-ui-keyword/expected-win32.png differ diff --git a/spec/fixtures/api/corner-smoothing/system-ui-keyword/test.html b/spec/fixtures/api/corner-smoothing/system-ui-keyword/test.html new file mode 100644 index 000000000000..ccb17205ec41 --- /dev/null +++ b/spec/fixtures/api/corner-smoothing/system-ui-keyword/test.html @@ -0,0 +1,42 @@ + + + + + + +
+
+
+ + \ No newline at end of file