Repository: electron/electron
Pull request: feat: support customizing window accent color on Windows

Backport of #47285

See that PR for details.


Notes: Added support for customizing system accent color and highlighting of active window border.


Changed files:
filename: docs/api/structures/base-window-options.md
patch: @@ -95,6 +95,7 @@
   * `color` String (optional) _Windows_ _Linux_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
   * `symbolColor` String (optional) _Windows_ _Linux_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.
   * `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels. Default is system height.
+* `accentColor` boolean | string (optional) _Windows_ - The accent color for the window. By default, follows user preference in System Settings. Set to `false` to explicitly disable, or set the color in Hex, RGB, RGBA, HSL, HSLA or named CSS color format. Alpha values will be ignored.
 * `trafficLightPosition` [Point](point.md) (optional) _macOS_ -
   Set a custom position for the traffic light buttons in frameless windows.
 * `roundedCorners` boolean (optional) _macOS_ _Windows_ - Whether frameless window

filename: shell/browser/native_window_views.cc
patch: @@ -79,6 +79,7 @@
 #include "base/win/windows_version.h"
 #include "shell/browser/ui/views/win_frame_view.h"
 #include "shell/browser/ui/win/electron_desktop_native_widget_aura.h"
+#include "shell/common/color_util.h"
 #include "skia/ext/skia_utils_win.h"
 #include "ui/display/win/screen_win.h"
 #include "ui/gfx/color_utils.h"
@@ -213,6 +214,14 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
 
   overlay_button_color_ = color_utils::GetSysSkColor(COLOR_BTNFACE);
   overlay_symbol_color_ = color_utils::GetSysSkColor(COLOR_BTNTEXT);
+
+  bool accent_color = true;
+  std::string accent_color_string;
+  if (options.Get(options::kAccentColor, &accent_color_string)) {
+    accent_color_ = ParseCSSColor(accent_color_string);
+  } else if (options.Get(options::kAccentColor, &accent_color)) {
+    accent_color_ = accent_color;
+  }
 #endif
 
   v8::Local<v8::Value> titlebar_overlay;
@@ -429,6 +438,8 @@ NativeWindowViews::NativeWindowViews(const gin_helper::Dictionary& options,
     last_window_state_ = ui::mojom::WindowShowState::kFullscreen;
   else
     last_window_state_ = ui::mojom::WindowShowState::kNormal;
+
+  UpdateWindowAccentColor();
 #endif
 
   // Listen to mouse events.

filename: shell/browser/native_window_views.h
patch: @@ -208,6 +208,7 @@ class NativeWindowViews : public NativeWindow,
   void ResetWindowControls();
   void SetRoundedCorners(bool rounded);
   void SetForwardMouseMessages(bool forward);
+  void UpdateWindowAccentColor();
   static LRESULT CALLBACK SubclassProc(HWND hwnd,
                                        UINT msg,
                                        WPARAM w_param,
@@ -304,6 +305,8 @@ class NativeWindowViews : public NativeWindow,
   // Whether the window is currently being moved.
   bool is_moving_ = false;
 
+  std::variant<bool, SkColor> accent_color_ = true;
+
   std::optional<gfx::Rect> pending_bounds_change_;
 
   // The message ID of the "TaskbarCreated" message, sent to us when we need to

filename: shell/browser/native_window_views_win.cc
patch: @@ -7,6 +7,7 @@
 #include <wrl/client.h>
 
 #include "base/win/atl.h"  // Must be before UIAutomationCore.h
+#include "base/win/registry.h"
 #include "base/win/scoped_handle.h"
 #include "base/win/windows_version.h"
 #include "content/public/browser/browser_accessibility_state.h"
@@ -28,6 +29,53 @@ namespace electron {
 
 namespace {
 
+void SetWindowBorderAndCaptionColor(HWND hwnd, COLORREF color) {
+  if (base::win::GetVersion() < base::win::Version::WIN11)
+    return;
+
+  HRESULT result =
+      DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, &color, sizeof(color));
+
+  if (FAILED(result))
+    LOG(WARNING) << "Failed to set caption color";
+
+  result =
+      DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &color, sizeof(color));
+
+  if (FAILED(result))
+    LOG(WARNING) << "Failed to set border color";
+}
+
+std::optional<DWORD> GetAccentColor() {
+  base::win::RegKey key;
+  if (key.Open(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\DWM",
+               KEY_READ) != ERROR_SUCCESS) {
+    return std::nullopt;
+  }
+
+  DWORD accent_color = 0;
+  if (key.ReadValueDW(L"AccentColor", &accent_color) != ERROR_SUCCESS) {
+    return std::nullopt;
+  }
+
+  return accent_color;
+}
+
+bool IsAccentColorOnTitleBarsEnabled() {
+  base::win::RegKey key;
+  if (key.Open(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\DWM",
+               KEY_READ) != ERROR_SUCCESS) {
+    return false;
+  }
+
+  DWORD enabled = 0;
+  if (key.ReadValueDW(L"ColorPrevalence", &enabled) != ERROR_SUCCESS) {
+    return false;
+  }
+
+  return enabled != 0;
+}
+
 // Convert Win32 WM_QUERYENDSESSIONS to strings.
 const std::vector<std::string> EndSessionToStringVec(LPARAM end_session_id) {
   std::vector<std::string> params;
@@ -448,6 +496,19 @@ bool NativeWindowViews::PreHandleMSG(UINT message,
       }
       return false;
     }
+    case WM_DWMCOLORIZATIONCOLORCHANGED: {
+      UpdateWindowAccentColor();
+      return false;
+    }
+    case WM_SETTINGCHANGE: {
+      if (l_param) {
+        const wchar_t* setting_name = reinterpret_cast<const wchar_t*>(l_param);
+        std::wstring setting_str(setting_name);
+        if (setting_str == L"ImmersiveColorSet")
+          UpdateWindowAccentColor();
+      }
+      return false;
+    }
     default: {
       return false;
     }
@@ -507,6 +568,35 @@ void NativeWindowViews::HandleSizeEvent(WPARAM w_param, LPARAM l_param) {
   }
 }
 
+void NativeWindowViews::UpdateWindowAccentColor() {
+  if (base::win::GetVersion() < base::win::Version::WIN11)
+    return;
+
+  if (!IsAccentColorOnTitleBarsEnabled())
+    return;
+
+  COLORREF border_color;
+  if (std::holds_alternative<bool>(accent_color_)) {
+    // Don't set accent color if the user has disabled it.
+    if (!std::get<bool>(accent_color_))
+      return;
+
+    std::optional<DWORD> accent_color = GetAccentColor();
+    if (!accent_color.has_value())
+      return;
+
+    border_color =
+        RGB(GetRValue(accent_color.value()), GetGValue(accent_color.value()),
+            GetBValue(accent_color.value()));
+  } else {
+    SkColor color = std::get<SkColor>(accent_color_);
+    border_color =
+        RGB(SkColorGetR(color), SkColorGetG(color), SkColorGetB(color));
+  }
+
+  SetWindowBorderAndCaptionColor(GetAcceleratedWidget(), border_color);
+}
+
 void NativeWindowViews::ResetWindowControls() {
   // If a given window was minimized and has since been
   // unminimized (restored/maximized), ensure the WCO buttons

filename: shell/common/options_switches.h
patch: @@ -123,6 +123,8 @@ inline constexpr std::string_view kRoundedCorners = "roundedCorners";
 
 inline constexpr std::string_view ktitleBarOverlay = "titleBarOverlay";
 
+inline constexpr std::string_view kAccentColor = "accentColor";
+
 // The color to use as the theme and symbol colors respectively for Window
 // Controls Overlay if enabled on Windows.
 inline constexpr std::string_view kOverlayButtonColor = "color";