Repository: electron/electron
Pull request: fix: background hover contrast for WCO buttons

#### Description of Change

Closes https://github.com/electron/electron/issues/48193
Refs CL:3519281

Fixes an issue where button background on mouse hover with `titleBarOverlay` wasn't always calculated to provide appropriate minimum contrast.
Switches to dynamic background hover color calculation using `ColorProvider` instead of hardcoding.

Before:
<img width="203" height="120" alt="Screenshot 2025-10-15 at 10 46 28 AM" src="https://github.com/user-attachments/assets/450b5db9-5b42-4545-8575-b84dc279d3a1" />

After:
<img width="184" height="133" alt="Screenshot 2025-10-15 at 10 45 36 AM" src="https://github.com/user-attachments/assets/ab99e570-947a-49b7-be24-897710aaa0d2" />

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where button background on mouse hover with `titleBarOverlay` wasn't always calculated to provide appropriate minimum contrast.


Changed files:
filename: shell/browser/ui/views/win_caption_button.cc
patch: @@ -11,6 +11,7 @@
 #include "base/i18n/rtl.h"
 #include "base/numerics/safe_conversions.h"
 #include "base/win/windows_version.h"
+#include "chrome/browser/ui/color/chrome_color_id.h"
 #include "chrome/grit/theme_resources.h"
 #include "shell/browser/native_window_views.h"
 #include "shell/browser/ui/views/win_frame_view.h"
@@ -47,6 +48,13 @@ std::unique_ptr<WinIconPainter> WinCaptionButton::CreateIconPainter() {
   return std::make_unique<WinIconPainter>();
 }
 
+SkColor WinCaptionButton::GetBaseForegroundColor() const {
+  return GetColorProvider()->GetColor(
+      frame_view_->GetShouldPaintAsActive()
+          ? kColorCaptionButtonForegroundActive
+          : kColorCaptionButtonForegroundInactive);
+}
+
 gfx::Size WinCaptionButton::CalculatePreferredSize(
     const views::SizeBounds& available_size) const {
   // TODO(bsep): The sizes in this function are for 1x device scale and don't
@@ -76,7 +84,7 @@ void WinCaptionButton::OnPaintBackground(gfx::Canvas* canvas) {
     pressed_alpha = 0x98;
   } else {
     // Match the native buttons.
-    base_color = frame_view_->GetReadableFeatureColor(bg_color);
+    base_color = GetBaseForegroundColor();
     hovered_alpha = 0x1A;
     pressed_alpha = 0x33;
 

filename: shell/browser/ui/views/win_caption_button.h
patch: @@ -49,6 +49,10 @@ class WinCaptionButton : public views::Button {
 
  private:
   std::unique_ptr<WinIconPainter> CreateIconPainter();
+
+  // The base color to use for the button symbols and background blending. Uses
+  // the more readable of black and white.
+  SkColor GetBaseForegroundColor() const;
   // Returns the amount we should visually reserve on the left (right in RTL)
   // for spacing between buttons. We do this instead of repositioning the
   // buttons to avoid the sliver of deadspace that would result.
@@ -59,10 +63,6 @@ class WinCaptionButton : public views::Button {
   // smaller indices).
   int GetButtonDisplayOrderIndex() const;
 
-  // The base color to use for the button symbols and background blending. Uses
-  // the more readable of black and white.
-  SkColor GetBaseColor() const;
-
   // Paints the minimize/maximize/restore/close icon for the button.
   void PaintSymbol(gfx::Canvas* canvas);
 

filename: shell/browser/ui/views/win_frame_view.cc
patch: @@ -39,18 +39,6 @@ void WinFrameView::Init(NativeWindowViews* window, views::Widget* frame) {
   }
 }
 
-SkColor WinFrameView::GetReadableFeatureColor(SkColor background_color) {
-  // color_utils::GetColorWithMaxContrast()/IsDark() aren't used here because
-  // they switch based on the Chrome light/dark endpoints, while we want to use
-  // the system native behavior below.
-  const auto windows_luma = [](SkColor c) {
-    return 0.25f * SkColorGetR(c) + 0.625f * SkColorGetG(c) +
-           0.125f * SkColorGetB(c);
-  };
-  return windows_luma(background_color) <= 128.0f ? SK_ColorWHITE
-                                                  : SK_ColorBLACK;
-}
-
 void WinFrameView::InvalidateCaptionButtons() {
   if (!caption_button_container_)
     return;
@@ -282,6 +270,10 @@ void WinFrameView::LayoutWindowControlsOverlay() {
   window()->NotifyLayoutWindowControlsOverlay();
 }
 
+bool WinFrameView::GetShouldPaintAsActive() {
+  return ShouldPaintAsActive();
+}
+
 BEGIN_METADATA(WinFrameView)
 END_METADATA
 

filename: shell/browser/ui/views/win_frame_view.h
patch: @@ -46,6 +46,9 @@ class WinFrameView : public FramelessView {
   // the area above the top of the screen).
   int TitlebarMaximizedVisualHeight() const;
 
+  // Returns true if the frame should be painted as active.
+  bool GetShouldPaintAsActive();
+
  protected:
   // views::View:
   void Layout(PassKey) override;