From f3774d578d03ed81c04a8d89f72aa677cecf5933 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:39:18 +0200 Subject: [PATCH] feat: add `{get|set}AccentColor` on Windows (#47939) * feat: add setAccentColor on Windows Co-authored-by: Shelley Vohr * refactor: unify GetSystemAccentColor Co-authored-by: Shelley Vohr * refactor: remove redundant parsing Co-authored-by: Shelley Vohr * chore: fixup documentation Co-authored-by: Shelley Vohr * Update docs/api/browser-window.md Co-authored-by: Will Anderson Co-authored-by: Shelley Vohr * Update docs/api/base-window.md Co-authored-by: Will Anderson Co-authored-by: Shelley Vohr --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Shelley Vohr --- docs/api/base-window.md | 37 ++++++++ docs/api/browser-window.md | 37 ++++++++ shell/browser/api/electron_api_base_window.cc | 29 +++++++ shell/browser/api/electron_api_base_window.h | 2 + .../api/electron_api_system_preferences.h | 2 +- .../electron_api_system_preferences_win.cc | 9 +- shell/browser/native_window.h | 4 + shell/browser/native_window_views.cc | 1 + shell/browser/native_window_views.h | 5 +- shell/browser/native_window_views_win.cc | 52 +++++++---- shell/common/color_util.cc | 15 ++++ shell/common/color_util.h | 10 +++ spec/api-browser-window-spec.ts | 86 +++++++++++++++++++ 13 files changed, 265 insertions(+), 24 deletions(-) diff --git a/docs/api/base-window.md b/docs/api/base-window.md index e56530eed58e..a4520c1c5748 100644 --- a/docs/api/base-window.md +++ b/docs/api/base-window.md @@ -1260,6 +1260,43 @@ Sets the properties for the window's taskbar button. > `relaunchCommand` and `relaunchDisplayName` must always be set > together. If one of those properties is not set, then neither will be used. +#### `win.setAccentColor(accentColor)` _Windows_ + +* `accentColor` boolean | string - The accent color for the window. By default, follows user preference in System Settings. + +Sets the system accent color and highlighting of active window border. + +The `accentColor` parameter accepts the following values: + +* **Color string** - Sets a custom accent color using standard CSS color formats (Hex, RGB, RGBA, HSL, HSLA, or named colors). Alpha values in RGBA/HSLA formats are ignored and the color is treated as fully opaque. +* **`true`** - Uses the system's default accent color from user preferences in System Settings. +* **`false`** - Explicitly disables accent color highlighting for the window. + +Examples: + +```js +const win = new BrowserWindow({ frame: false }) + +// Set red accent color. +win.setAccentColor('#ff0000') + +// RGB format (alpha ignored if present). +win.setAccentColor('rgba(255,0,0,0.5)') + +// Use system accent color. +win.setAccentColor(true) + +// Disable accent color. +win.setAccentColor(false) +``` + +#### `win.getAccentColor()` _Windows_ + +Returns `string | boolean` - the system accent color and highlighting of active window border in Hex RGB format. + +If a color has been set for the window that differs from the system accent color, the window accent color will +be returned. Otherwise, a boolean will be returned, with `true` indicating that the window uses the global system accent color, and `false` indicating that accent color highlighting is disabled for this window. + #### `win.setIcon(icon)` _Windows_ _Linux_ * `icon` [NativeImage](native-image.md) | string diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 4d1ba3cbc9ed..4578ca071385 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -1440,6 +1440,43 @@ Sets the properties for the window's taskbar button. > `relaunchCommand` and `relaunchDisplayName` must always be set > together. If one of those properties is not set, then neither will be used. +#### `win.setAccentColor(accentColor)` _Windows_ + +* `accentColor` boolean | string - The accent color for the window. By default, follows user preference in System Settings. + +Sets the system accent color and highlighting of active window border. + +The `accentColor` parameter accepts the following values: + +* **Color string** - Sets a custom accent color using standard CSS color formats (Hex, RGB, RGBA, HSL, HSLA, or named colors). Alpha values in RGBA/HSLA formats are ignored and the color is treated as fully opaque. +* **`true`** - Uses the system's default accent color from user preferences in System Settings. +* **`false`** - Explicitly disables accent color highlighting for the window. + +Examples: + +```js +const win = new BrowserWindow({ frame: false }) + +// Set red accent color. +win.setAccentColor('#ff0000') + +// RGB format (alpha ignored if present). +win.setAccentColor('rgba(255,0,0,0.5)') + +// Use system accent color. +win.setAccentColor(true) + +// Disable accent color. +win.setAccentColor(false) +``` + +#### `win.getAccentColor()` _Windows_ + +Returns `string | boolean` - the system accent color and highlighting of active window border in Hex RGB format. + +If a color has been set for the window that differs from the system accent color, the window accent color will +be returned. Otherwise, a boolean will be returned, with `true` indicating that the window uses the global system accent color, and `false` indicating that accent color highlighting is disabled for this window. + #### `win.showDefinitionForSelection()` _macOS_ Same as `webContents.showDefinitionForSelection()`. diff --git a/shell/browser/api/electron_api_base_window.cc b/shell/browser/api/electron_api_base_window.cc index fd7ba60029bf..4b09ec419a5e 100644 --- a/shell/browser/api/electron_api_base_window.cc +++ b/shell/browser/api/electron_api_base_window.cc @@ -1089,6 +1089,33 @@ void BaseWindow::SetAppDetails(const gin_helper::Dictionary& options) { bool BaseWindow::IsSnapped() const { return window_->IsSnapped(); } + +void BaseWindow::SetAccentColor(gin_helper::Arguments* args) { + bool accent_color = false; + std::string accent_color_string; + if (args->GetNext(&accent_color_string)) { + std::optional maybe_color = ParseCSSColor(accent_color_string); + if (maybe_color.has_value()) { + window_->SetAccentColor(maybe_color.value()); + window_->UpdateWindowAccentColor(window_->IsActive()); + } + } else if (args->GetNext(&accent_color)) { + window_->SetAccentColor(accent_color); + window_->UpdateWindowAccentColor(window_->IsActive()); + } else { + args->ThrowError( + "Invalid accent color value - must be a string or boolean"); + } +} + +v8::Local BaseWindow::GetAccentColor() const { + v8::Isolate* isolate = v8::Isolate::GetCurrent(); + auto accent_color = window_->GetAccentColor(); + + if (std::holds_alternative(accent_color)) + return v8::Boolean::New(isolate, std::get(accent_color)); + return gin::StringToV8(isolate, std::get(accent_color)); +} #endif #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) @@ -1277,6 +1304,8 @@ void BaseWindow::BuildPrototype(v8::Isolate* isolate, #if BUILDFLAG(IS_WIN) .SetMethod("isSnapped", &BaseWindow::IsSnapped) .SetProperty("snapped", &BaseWindow::IsSnapped) + .SetMethod("setAccentColor", &BaseWindow::SetAccentColor) + .SetMethod("getAccentColor", &BaseWindow::GetAccentColor) .SetMethod("hookWindowMessage", &BaseWindow::HookWindowMessage) .SetMethod("isWindowMessageHooked", &BaseWindow::IsWindowMessageHooked) .SetMethod("unhookWindowMessage", &BaseWindow::UnhookWindowMessage) diff --git a/shell/browser/api/electron_api_base_window.h b/shell/browser/api/electron_api_base_window.h index 103f33c357cb..3aff31d9054c 100644 --- a/shell/browser/api/electron_api_base_window.h +++ b/shell/browser/api/electron_api_base_window.h @@ -255,6 +255,8 @@ class BaseWindow : public gin_helper::TrackableObject, bool SetThumbnailToolTip(const std::string& tooltip); void SetAppDetails(const gin_helper::Dictionary& options); bool IsSnapped() const; + void SetAccentColor(gin_helper::Arguments* args); + v8::Local GetAccentColor() const; #endif #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) diff --git a/shell/browser/api/electron_api_system_preferences.h b/shell/browser/api/electron_api_system_preferences.h index 2e2758511de0..25c70b5e09f9 100644 --- a/shell/browser/api/electron_api_system_preferences.h +++ b/shell/browser/api/electron_api_system_preferences.h @@ -56,7 +56,7 @@ class SystemPreferences final const char* GetTypeName() override; #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) - std::string GetAccentColor(); + static std::string GetAccentColor(); std::string GetColor(gin_helper::ErrorThrower thrower, const std::string& color); std::string GetMediaAccessStatus(gin_helper::ErrorThrower thrower, diff --git a/shell/browser/api/electron_api_system_preferences_win.cc b/shell/browser/api/electron_api_system_preferences_win.cc index 40454bb7f972..d03f0f0c3e1b 100644 --- a/shell/browser/api/electron_api_system_preferences_win.cc +++ b/shell/browser/api/electron_api_system_preferences_win.cc @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -84,14 +83,12 @@ std::string hexColorDWORDToRGBA(DWORD color) { } std::string SystemPreferences::GetAccentColor() { - DWORD color = 0; - BOOL opaque = FALSE; + std::optional color = GetSystemAccentColor(); - if (FAILED(DwmGetColorizationColor(&color, &opaque))) { + if (!color.has_value()) return ""; - } - return hexColorDWORDToRGBA(color); + return hexColorDWORDToRGBA(color.value()); } std::string SystemPreferences::GetColor(gin_helper::ErrorThrower thrower, diff --git a/shell/browser/native_window.h b/shell/browser/native_window.h index 42a239a65b04..2c25fa547ab7 100644 --- a/shell/browser/native_window.h +++ b/shell/browser/native_window.h @@ -347,6 +347,10 @@ class NativeWindow : public base::SupportsUserData, #if BUILDFLAG(IS_WIN) void NotifyWindowMessage(UINT message, WPARAM w_param, LPARAM l_param); + virtual void SetAccentColor( + std::variant accent_color) = 0; + virtual std::variant GetAccentColor() const = 0; + virtual void UpdateWindowAccentColor(bool active) = 0; #endif void AddObserver(NativeWindowObserver* obs) { observers_.AddObserver(obs); } diff --git a/shell/browser/native_window_views.cc b/shell/browser/native_window_views.cc index c7f8916177f5..ceee28bfc768 100644 --- a/shell/browser/native_window_views.cc +++ b/shell/browser/native_window_views.cc @@ -26,6 +26,7 @@ #include "base/strings/utf_string_conversions.h" #include "content/public/browser/desktop_media_id.h" #include "content/public/common/color_parser.h" +#include "shell/browser/api/electron_api_system_preferences.h" #include "shell/browser/api/electron_api_web_contents.h" #include "shell/browser/ui/inspectable_web_contents_view.h" #include "shell/browser/ui/views/root_view.h" diff --git a/shell/browser/native_window_views.h b/shell/browser/native_window_views.h index ae85a927fc7f..ffe4e869b922 100644 --- a/shell/browser/native_window_views.h +++ b/shell/browser/native_window_views.h @@ -176,6 +176,10 @@ class NativeWindowViews : public NativeWindow, #endif #if BUILDFLAG(IS_WIN) + void SetAccentColor( + std::variant accent_color) override; + std::variant GetAccentColor() const override; + void UpdateWindowAccentColor(bool active) override; TaskbarHost& taskbar_host() { return taskbar_host_; } void UpdateThickFrame(); void SetLayered(); @@ -223,7 +227,6 @@ class NativeWindowViews : public NativeWindow, void ResetWindowControls(); void SetRoundedCorners(bool rounded); void SetForwardMouseMessages(bool forward); - void UpdateWindowAccentColor(bool active); static LRESULT CALLBACK SubclassProc(HWND hwnd, UINT msg, WPARAM w_param, diff --git a/shell/browser/native_window_views_win.cc b/shell/browser/native_window_views_win.cc index 7baa349bd91f..028faa91429b 100644 --- a/shell/browser/native_window_views_win.cc +++ b/shell/browser/native_window_views_win.cc @@ -16,7 +16,9 @@ #include "shell/browser/native_window_views.h" #include "shell/browser/ui/views/root_view.h" #include "shell/browser/ui/views/win_frame_view.h" +#include "shell/common/color_util.h" #include "shell/common/electron_constants.h" +#include "skia/ext/skia_utils_win.h" #include "ui/display/display.h" #include "ui/display/screen.h" #include "ui/gfx/geometry/resize_utils.h" @@ -46,21 +48,6 @@ void SetWindowBorderAndCaptionColor(HWND hwnd, COLORREF color) { LOG(WARNING) << "Failed to set border color"; } -std::optional 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", @@ -594,7 +581,7 @@ void NativeWindowViews::UpdateWindowAccentColor(bool active) { // Use system accent color as fallback if no explicit color was set. if (!border_color.has_value() && should_apply_accent) { - std::optional system_accent_color = GetAccentColor(); + std::optional system_accent_color = GetSystemAccentColor(); if (system_accent_color.has_value()) { border_color = RGB(GetRValue(system_accent_color.value()), GetGValue(system_accent_color.value()), @@ -606,6 +593,39 @@ void NativeWindowViews::UpdateWindowAccentColor(bool active) { SetWindowBorderAndCaptionColor(GetAcceleratedWidget(), final_color); } +void NativeWindowViews::SetAccentColor( + std::variant accent_color) { + accent_color_ = accent_color; +} + +/* + * Returns the window's accent color, per the following heuristic: + * + * - If |accent_color_| is an SkColor, return that color as a hex string. + * - If |accent_color_| is true, return the system accent color as a hex string. + * - If |accent_color_| is false, return false. + * - Otherwise, return the system accent color as a hex string. + */ +std::variant NativeWindowViews::GetAccentColor() const { + std::optional system_color = GetSystemAccentColor(); + + if (std::holds_alternative(accent_color_)) { + return ToRGBHex(std::get(accent_color_)); + } else if (std::holds_alternative(accent_color_)) { + if (std::get(accent_color_)) { + if (!system_color.has_value()) + return false; + return ToRGBHex(skia::COLORREFToSkColor(system_color.value())); + } else { + return false; + } + } else { + if (!system_color.has_value()) + return false; + return ToRGBHex(skia::COLORREFToSkColor(system_color.value())); + } +} + void NativeWindowViews::ResetWindowControls() { // If a given window was minimized and has since been // unminimized (restored/maximized), ensure the WCO buttons diff --git a/shell/common/color_util.cc b/shell/common/color_util.cc index 66669a38c5bb..c0b3f68367d5 100644 --- a/shell/common/color_util.cc +++ b/shell/common/color_util.cc @@ -10,6 +10,10 @@ #include "content/public/common/color_parser.h" #include "third_party/abseil-cpp/absl/strings/str_format.h" +#if BUILDFLAG(IS_WIN) +#include +#endif + namespace { bool IsHexFormatWithAlpha(const std::string& str) { @@ -62,4 +66,15 @@ std::string ToRGBAHex(SkColor color, bool include_hash) { return color_str; } +#if BUILDFLAG(IS_WIN) +std::optional GetSystemAccentColor() { + DWORD color = 0; + BOOL opaque = FALSE; + + if (FAILED(DwmGetColorizationColor(&color, &opaque))) + return std::nullopt; + return color; +} +#endif + } // namespace electron diff --git a/shell/common/color_util.h b/shell/common/color_util.h index c1e880d8fe4a..873ec6779025 100644 --- a/shell/common/color_util.h +++ b/shell/common/color_util.h @@ -8,6 +8,12 @@ #include #include +#include "build/build_config.h" + +#if BUILDFLAG(IS_WIN) +#include +#endif + #include "third_party/skia/include/core/SkColor.h" // SkColor is a typedef for uint32_t, this wrapper is to tag an SkColor for @@ -31,6 +37,10 @@ std::string ToRGBHex(SkColor color); // Convert color to RGBA hex value like "#RRGGBBAA". std::string ToRGBAHex(SkColor color, bool include_hash = true); +#if BUILDFLAG(IS_WIN) +std::optional GetSystemAccentColor(); +#endif + } // namespace electron #endif // ELECTRON_SHELL_COMMON_COLOR_UTIL_H_ diff --git a/spec/api-browser-window-spec.ts b/spec/api-browser-window-spec.ts index 326f098f78f5..58529ac1f837 100755 --- a/spec/api-browser-window-spec.ts +++ b/spec/api-browser-window-spec.ts @@ -2554,6 +2554,92 @@ describe('BrowserWindow module', () => { }); }); + ifdescribe(process.platform === 'win32')('BrowserWindow.{get|set}AccentColor', () => { + afterEach(closeAllWindows); + + it('throws if called with an invalid parameter', () => { + const w = new BrowserWindow({ show: false }); + expect(() => { + // @ts-ignore this is wrong on purpose. + w.setAccentColor([1, 2, 3]); + }).to.throw('Invalid accent color value - must be a string or boolean'); + }); + + it('returns the accent color after setting it to a string', () => { + const w = new BrowserWindow({ show: false }); + const testColor = '#FF0000'; + w.setAccentColor(testColor); + const accentColor = w.getAccentColor(); + expect(accentColor).to.be.a('string'); + expect(accentColor).to.equal(testColor); + }); + + it('returns the accent color after setting it to false', () => { + const w = new BrowserWindow({ show: false }); + w.setAccentColor(false); + const accentColor = w.getAccentColor(); + expect(accentColor).to.be.a('boolean'); + expect(accentColor).to.equal(false); + }); + + it('returns a system color when set to true', () => { + const w = new BrowserWindow({ show: false }); + w.setAccentColor(true); + const accentColor = w.getAccentColor(); + expect(accentColor).to.be.a('string'); + expect(accentColor).to.match(/^#[0-9A-F]{6}$/i); + }); + + it('returns the correct accent color after multiple changes', () => { + const w = new BrowserWindow({ show: false }); + + const testColor1 = '#00FF00'; + w.setAccentColor(testColor1); + expect(w.getAccentColor()).to.equal(testColor1); + + w.setAccentColor(false); + expect(w.getAccentColor()).to.equal(false); + + const testColor2 = '#0000FF'; + w.setAccentColor(testColor2); + expect(w.getAccentColor()).to.equal(testColor2); + + w.setAccentColor(true); + const systemColor = w.getAccentColor(); + expect(systemColor).to.be.a('string'); + expect(systemColor).to.match(/^#[0-9A-F]{6}$/i); + }); + + it('handles CSS color names correctly', () => { + const w = new BrowserWindow({ show: false }); + const testColor = 'red'; + w.setAccentColor(testColor); + const accentColor = w.getAccentColor(); + expect(accentColor).to.be.a('string'); + expect(accentColor).to.equal('#FF0000'); + }); + + it('handles RGB color values correctly', () => { + const w = new BrowserWindow({ show: false }); + const testColor = 'rgb(255, 128, 0)'; + w.setAccentColor(testColor); + const accentColor = w.getAccentColor(); + expect(accentColor).to.be.a('string'); + expect(accentColor).to.equal('#FF8000'); + }); + + it('persists accent color across window operations', () => { + const w = new BrowserWindow({ show: false }); + const testColor = '#ABCDEF'; + w.setAccentColor(testColor); + + w.show(); + w.hide(); + + expect(w.getAccentColor()).to.equal(testColor); + }); + }); + describe('BrowserWindow.setAlwaysOnTop(flag, level)', () => { let w: BrowserWindow;