Browse Source

fix: add theme data source for devtools. (#44636)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Bill Shen <[email protected]>
trop[bot] 5 months ago
parent
commit
da390c193b

+ 1 - 0
chromium_src/BUILD.gn

@@ -182,6 +182,7 @@ static_library("chrome") {
   public_deps = [
     "//chrome/browser:dev_ui_browser_resources",
     "//chrome/browser/resources/accessibility:resources",
+    "//chrome/browser/ui/color:color_headers",
     "//chrome/browser/ui/color:mixers",
     "//chrome/common",
     "//chrome/common:version_header",

+ 4 - 0
filenames.gni

@@ -507,6 +507,10 @@ filenames = {
     "shell/browser/ui/devtools_manager_delegate.h",
     "shell/browser/ui/devtools_ui.cc",
     "shell/browser/ui/devtools_ui.h",
+    "shell/browser/ui/devtools_ui_bundle_data_source.cc",
+    "shell/browser/ui/devtools_ui_bundle_data_source.h",
+    "shell/browser/ui/devtools_ui_theme_data_source.cc",
+    "shell/browser/ui/devtools_ui_theme_data_source.h",
     "shell/browser/ui/drag_util.cc",
     "shell/browser/ui/drag_util.h",
     "shell/browser/ui/electron_menu_model.cc",

+ 4 - 110
shell/browser/ui/devtools_ui.cc

@@ -5,127 +5,21 @@
 #include "shell/browser/ui/devtools_ui.h"
 
 #include <memory>
-#include <string>
-#include <utility>
 
-#include "base/memory/ref_counted_memory.h"
-#include "base/strings/strcat.h"
-#include "base/strings/string_util.h"
-#include "chrome/common/webui_url_constants.h"
-#include "content/public/browser/devtools_frontend_host.h"
-#include "content/public/browser/url_data_source.h"
-#include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui.h"
+#include "shell/browser/ui/devtools_ui_bundle_data_source.h"
+#include "shell/browser/ui/devtools_ui_theme_data_source.h"
 
 namespace electron {
 
-namespace {
-
-std::string PathWithoutParams(const std::string& path) {
-  return GURL(base::StrCat({content::kChromeDevToolsScheme,
-                            url::kStandardSchemeSeparator,
-                            chrome::kChromeUIDevToolsHost}))
-      .Resolve(path)
-      .path()
-      .substr(1);
-}
-
-std::string GetMimeTypeForUrl(const GURL& url) {
-  std::string filename = url.ExtractFileName();
-  if (base::EndsWith(filename, ".html", base::CompareCase::INSENSITIVE_ASCII)) {
-    return "text/html";
-  } else if (base::EndsWith(filename, ".css",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "text/css";
-  } else if (base::EndsWith(filename, ".js",
-                            base::CompareCase::INSENSITIVE_ASCII) ||
-             base::EndsWith(filename, ".mjs",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "application/javascript";
-  } else if (base::EndsWith(filename, ".png",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "image/png";
-  } else if (base::EndsWith(filename, ".map",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "application/json";
-  } else if (base::EndsWith(filename, ".ts",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "application/x-typescript";
-  } else if (base::EndsWith(filename, ".gif",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "image/gif";
-  } else if (base::EndsWith(filename, ".svg",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "image/svg+xml";
-  } else if (base::EndsWith(filename, ".manifest",
-                            base::CompareCase::INSENSITIVE_ASCII)) {
-    return "text/cache-manifest";
-  }
-  return "text/html";
-}
-
-class BundledDataSource : public content::URLDataSource {
- public:
-  BundledDataSource() = default;
-  ~BundledDataSource() override = default;
-
-  // disable copy
-  BundledDataSource(const BundledDataSource&) = delete;
-  BundledDataSource& operator=(const BundledDataSource&) = delete;
-
-  // content::URLDataSource implementation.
-  std::string GetSource() override { return chrome::kChromeUIDevToolsHost; }
-
-  void StartDataRequest(const GURL& url,
-                        const content::WebContents::Getter& wc_getter,
-                        GotDataCallback callback) override {
-    const std::string path = content::URLDataSource::URLToRequestPath(url);
-    // Serve request from local bundle.
-    std::string bundled_path_prefix(chrome::kChromeUIDevToolsBundledPath);
-    bundled_path_prefix += "/";
-    if (base::StartsWith(path, bundled_path_prefix,
-                         base::CompareCase::INSENSITIVE_ASCII)) {
-      StartBundledDataRequest(path.substr(bundled_path_prefix.length()),
-                              std::move(callback));
-      return;
-    }
-
-    // We do not handle remote and custom requests.
-    std::move(callback).Run(nullptr);
-  }
-
-  std::string GetMimeType(const GURL& url) override {
-    return GetMimeTypeForUrl(url);
-  }
-
-  bool ShouldAddContentSecurityPolicy() override { return false; }
-
-  bool ShouldDenyXFrameOptions() override { return false; }
-
-  bool ShouldServeMimeTypeAsContentTypeHeader() override { return true; }
-
-  void StartBundledDataRequest(const std::string& path,
-                               GotDataCallback callback) {
-    std::string filename = PathWithoutParams(path);
-    scoped_refptr<base::RefCountedMemory> bytes =
-        content::DevToolsFrontendHost::GetFrontendResourceBytes(filename);
-
-    DLOG_IF(WARNING, !bytes)
-        << "Unable to find dev tool resource: " << filename
-        << ". If you compiled with debug_devtools=1, try running with "
-           "--debug-devtools.";
-    std::move(callback).Run(bytes);
-  }
-};
-
-}  // namespace
-
 DevToolsUI::DevToolsUI(content::BrowserContext* browser_context,
                        content::WebUI* web_ui)
     : WebUIController(web_ui) {
   web_ui->SetBindings(content::BindingsPolicySet());
   content::URLDataSource::Add(browser_context,
                               std::make_unique<BundledDataSource>());
+  content::URLDataSource::Add(browser_context,
+                              std::make_unique<ThemeDataSource>());
 }
 
 }  // namespace electron

+ 1 - 0
shell/browser/ui/devtools_ui.h

@@ -14,6 +14,7 @@ class DevToolsUI : public content::WebUIController {
  public:
   explicit DevToolsUI(content::BrowserContext* browser_context,
                       content::WebUI* web_ui);
+  ~DevToolsUI() override = default;
 
   // disable copy
   DevToolsUI(const DevToolsUI&) = delete;

+ 136 - 0
shell/browser/ui/devtools_ui_bundle_data_source.cc

@@ -0,0 +1,136 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/ui/devtools_ui_bundle_data_source.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/memory/ref_counted_memory.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/ui/color/chrome_color_id.h"
+#include "chrome/browser/ui/color/chrome_color_provider_utils.h"
+#include "chrome/common/webui_url_constants.h"
+#include "content/public/browser/devtools_frontend_host.h"
+#include "content/public/browser/url_data_source.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_ui.h"
+#include "net/base/url_util.h"
+#include "ui/base/webui/web_ui_util.h"
+#include "ui/color/color_provider.h"
+#include "ui/color/color_provider_utils.h"
+
+namespace electron {
+namespace {
+std::string PathWithoutParams(const std::string& path) {
+  return GURL(base::StrCat({content::kChromeDevToolsScheme,
+                            url::kStandardSchemeSeparator,
+                            chrome::kChromeUIDevToolsHost}))
+      .Resolve(path)
+      .path()
+      .substr(1);
+}
+
+scoped_refptr<base::RefCountedMemory> CreateNotFoundResponse() {
+  const char kHttpNotFound[] = "HTTP/1.1 404 Not Found\n\n";
+  return base::MakeRefCounted<base::RefCountedStaticMemory>(
+      base::byte_span_from_cstring(kHttpNotFound));
+}
+
+std::string GetMimeTypeForUrl(const GURL& url) {
+  std::string filename = url.ExtractFileName();
+  if (base::EndsWith(filename, ".html", base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/html";
+  } else if (base::EndsWith(filename, ".css",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/css";
+  } else if (base::EndsWith(filename, ".js",
+                            base::CompareCase::INSENSITIVE_ASCII) ||
+             base::EndsWith(filename, ".mjs",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/javascript";
+  } else if (base::EndsWith(filename, ".png",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/png";
+  } else if (base::EndsWith(filename, ".map",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/json";
+  } else if (base::EndsWith(filename, ".ts",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/x-typescript";
+  } else if (base::EndsWith(filename, ".gif",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/gif";
+  } else if (base::EndsWith(filename, ".svg",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/svg+xml";
+  } else if (base::EndsWith(filename, ".manifest",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/cache-manifest";
+  }
+  return "text/html";
+}
+}  // namespace
+
+#pragma mark - BundledDataSource
+
+std::string BundledDataSource::GetSource() {
+  // content::URLDataSource implementation.
+  return chrome::kChromeUIDevToolsHost;
+}
+
+void BundledDataSource::StartDataRequest(
+    const GURL& url,
+    const content::WebContents::Getter& wc_getter,
+    GotDataCallback callback) {
+  const std::string path = content::URLDataSource::URLToRequestPath(url);
+  // Serve request from local bundle.
+  std::string bundled_path_prefix(chrome::kChromeUIDevToolsBundledPath);
+  bundled_path_prefix += "/";
+  if (base::StartsWith(path, bundled_path_prefix,
+                       base::CompareCase::INSENSITIVE_ASCII)) {
+    StartBundledDataRequest(path.substr(bundled_path_prefix.length()),
+                            std::move(callback));
+    return;
+  }
+
+  // We do not handle remote and custom requests.
+  std::move(callback).Run(CreateNotFoundResponse());
+}
+
+std::string BundledDataSource::GetMimeType(const GURL& url) {
+  return GetMimeTypeForUrl(url);
+}
+
+bool BundledDataSource::ShouldAddContentSecurityPolicy() {
+  return false;
+}
+
+bool BundledDataSource::ShouldDenyXFrameOptions() {
+  return false;
+}
+
+bool BundledDataSource::ShouldServeMimeTypeAsContentTypeHeader() {
+  return true;
+}
+
+void BundledDataSource::StartBundledDataRequest(const std::string& path,
+                                                GotDataCallback callback) {
+  std::string filename = PathWithoutParams(path);
+  scoped_refptr<base::RefCountedMemory> bytes =
+      content::DevToolsFrontendHost::GetFrontendResourceBytes(filename);
+
+  DLOG_IF(WARNING, !bytes)
+      << "Unable to find dev tool resource: " << filename
+      << ". If you compiled with debug_devtools=1, try running with "
+         "--debug-devtools.";
+  std::move(callback).Run(bytes);
+}
+
+}  // namespace electron

+ 40 - 0
shell/browser/ui/devtools_ui_bundle_data_source.h

@@ -0,0 +1,40 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_BUNDLE_DATA_SOURCE_H_
+#define ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_BUNDLE_DATA_SOURCE_H_
+
+#include "content/public/browser/url_data_source.h"
+
+namespace electron {
+// A BundledDataSource implementation that handles devtools://devtools/
+// requests.
+class BundledDataSource : public content::URLDataSource {
+ public:
+  BundledDataSource() = default;
+  ~BundledDataSource() override = default;
+
+  // disable copy
+  BundledDataSource(const BundledDataSource&) = delete;
+  BundledDataSource& operator=(const BundledDataSource&) = delete;
+
+  std::string GetSource() override;
+
+  void StartDataRequest(const GURL& url,
+                        const content::WebContents::Getter& wc_getter,
+                        GotDataCallback callback) override;
+
+ private:
+  std::string GetMimeType(const GURL& url) override;
+
+  bool ShouldAddContentSecurityPolicy() override;
+  bool ShouldDenyXFrameOptions() override;
+  bool ShouldServeMimeTypeAsContentTypeHeader() override;
+
+  void StartBundledDataRequest(const std::string& path,
+                               GotDataCallback callback);
+};
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_BUNDLE_DATA_SOURCE_H_

+ 220 - 0
shell/browser/ui/devtools_ui_theme_data_source.cc

@@ -0,0 +1,220 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/ui/devtools_ui_theme_data_source.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/memory/ref_counted_memory.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/ui/color/chrome_color_id.h"
+#include "chrome/browser/ui/color/chrome_color_provider_utils.h"
+#include "chrome/common/webui_url_constants.h"
+#include "content/public/browser/devtools_frontend_host.h"
+#include "content/public/browser/url_data_source.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_ui.h"
+#include "net/base/url_util.h"
+#include "ui/base/webui/web_ui_util.h"
+#include "ui/color/color_provider.h"
+#include "ui/color/color_provider_utils.h"
+
+namespace electron {
+
+namespace {
+GURL GetThemeUrl(const std::string& path) {
+  return GURL(std::string(content::kChromeDevToolsScheme) +
+              url::kStandardSchemeSeparator +
+              std::string(chrome::kChromeUIThemeHost) + "/" + path);
+}
+
+scoped_refptr<base::RefCountedMemory> CreateNotFoundResponse() {
+  const char kHttpNotFound[] = "HTTP/1.1 404 Not Found\n\n";
+  return base::MakeRefCounted<base::RefCountedStaticMemory>(
+      base::byte_span_from_cstring(kHttpNotFound));
+}
+
+std::string GetMimeTypeForUrl(const GURL& url) {
+  std::string filename = url.ExtractFileName();
+  if (base::EndsWith(filename, ".html", base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/html";
+  } else if (base::EndsWith(filename, ".css",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/css";
+  } else if (base::EndsWith(filename, ".js",
+                            base::CompareCase::INSENSITIVE_ASCII) ||
+             base::EndsWith(filename, ".mjs",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/javascript";
+  } else if (base::EndsWith(filename, ".png",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/png";
+  } else if (base::EndsWith(filename, ".map",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/json";
+  } else if (base::EndsWith(filename, ".ts",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "application/x-typescript";
+  } else if (base::EndsWith(filename, ".gif",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/gif";
+  } else if (base::EndsWith(filename, ".svg",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "image/svg+xml";
+  } else if (base::EndsWith(filename, ".manifest",
+                            base::CompareCase::INSENSITIVE_ASCII)) {
+    return "text/cache-manifest";
+  }
+  return "text/html";
+}
+}  // namespace
+
+std::string ThemeDataSource::GetSource() {
+  // kChromeUIThemeHost
+  return chrome::kChromeUIThemeHost;
+}
+
+void ThemeDataSource::StartDataRequest(
+    const GURL& url,
+    const content::WebContents::Getter& wc_getter,
+    GotDataCallback callback) {
+  // TODO(crbug.com/40050262): Simplify usages of |path| since |url| is
+  // available.
+  const std::string path = content::URLDataSource::URLToRequestPath(url);
+  // Default scale factor if not specified.
+  float scale = 1.0f;
+  // All frames by default if not specified.
+  int frame = -1;
+  std::string parsed_path;
+  webui::ParsePathAndImageSpec(GetThemeUrl(path), &parsed_path, &scale, &frame);
+
+  // kColorsCssPath should stay consistent with COLORS_CSS_SELECTOR in
+  // colors_css_updater.js.
+  constexpr char kColorsCssPath[] = "colors.css";
+  if (parsed_path == kColorsCssPath) {
+    SendColorsCss(url, wc_getter, std::move(callback));
+    return;
+  }
+
+  std::move(callback).Run(CreateNotFoundResponse());
+}
+
+std::string ThemeDataSource::GetMimeType(const GURL& url) {
+  return GetMimeTypeForUrl(url);
+}
+
+void ThemeDataSource::SendColorsCss(
+    const GURL& url,
+    const content::WebContents::Getter& wc_getter,
+    content::URLDataSource::GotDataCallback callback) {
+  const ui::ColorProvider& color_provider = wc_getter.Run()->GetColorProvider();
+
+  std::string sets_param;
+  std::vector<std::string_view> color_id_sets;
+  bool generate_rgb_vars = false;
+  std::string generate_rgb_vars_query_value;
+  if (net::GetValueForKeyInQuery(url, "generate_rgb_vars",
+                                 &generate_rgb_vars_query_value)) {
+    generate_rgb_vars =
+        base::ToLowerASCII(generate_rgb_vars_query_value) == "true";
+  }
+  bool shadow_host = false;
+  std::string shadow_host_query_value;
+  if (net::GetValueForKeyInQuery(url, "shadow_host",
+                                 &shadow_host_query_value)) {
+    shadow_host = base::ToLowerASCII(shadow_host_query_value) == "true";
+  }
+  if (!net::GetValueForKeyInQuery(url, "sets", &sets_param)) {
+    LOG(ERROR)
+        << "colors.css requires a 'sets' query parameter to specify the "
+           "color "
+           "id sets returned e.g chrome://theme/colors.css?sets=ui,chrome";
+    std::move(callback).Run(nullptr);
+    return;
+  }
+  color_id_sets = base::SplitStringPiece(sets_param, ",", base::TRIM_WHITESPACE,
+                                         base::SPLIT_WANT_ALL);
+
+  using ColorIdCSSCallback = base::RepeatingCallback<std::string(ui::ColorId)>;
+  auto generate_color_mapping =
+      [&color_id_sets, &color_provider, &generate_rgb_vars](
+          std::string set_name, ui::ColorId start, ui::ColorId end,
+          ColorIdCSSCallback color_css_name) {
+        // Only return these mappings if specified in the query parameter.
+        auto it = base::ranges::find(color_id_sets, set_name);
+        if (it == color_id_sets.end()) {
+          return std::string();
+        }
+        color_id_sets.erase(it);
+        std::string css_string;
+        for (ui::ColorId id = start; id < end; ++id) {
+          const SkColor color = color_provider.GetColor(id);
+          std::string css_id_to_color_mapping =
+              base::StringPrintf("%s:%s;", color_css_name.Run(id).c_str(),
+                                 ui::ConvertSkColorToCSSColor(color).c_str());
+          base::StrAppend(&css_string, {css_id_to_color_mapping});
+          if (generate_rgb_vars) {
+            // Also generate a r,g,b string for each color so apps can construct
+            // colors with their own opacities in css.
+            const std::string css_rgb_color_str =
+                color_utils::SkColorToRgbString(color);
+            const std::string css_id_to_rgb_color_mapping =
+                base::StringPrintf("%s-rgb:%s;", color_css_name.Run(id).c_str(),
+                                   css_rgb_color_str.c_str());
+            base::StrAppend(&css_string, {css_id_to_rgb_color_mapping});
+          }
+        }
+        return css_string;
+      };
+
+  // Convenience lambda for wrapping
+  // |ConvertColorProviderColorIdToCSSColorId|.
+  auto generate_color_provider_mapping = [&generate_color_mapping](
+                                             std::string set_name,
+                                             ui::ColorId start, ui::ColorId end,
+                                             std::string (*color_id_name)(
+                                                 ui::ColorId)) {
+    auto color_id_to_css_name = base::BindRepeating(
+        [](std::string (*color_id_name)(ui::ColorId), ui::ColorId id) {
+          return ui::ConvertColorProviderColorIdToCSSColorId(color_id_name(id));
+        },
+        color_id_name);
+    return generate_color_mapping(set_name, start, end, color_id_to_css_name);
+  };
+
+  std::string css_selector;
+  if (shadow_host) {
+    css_selector = ":host";
+  } else {
+    // This selector requires more specificity than other existing CSS
+    // selectors that define variables. We increase the specifity by adding
+    // a pseudoselector.
+    css_selector = "html:not(#z)";
+  }
+
+  std::string css_string = base::StrCat(
+      {css_selector, "{", "--user-color-source: baseline-default;",
+       generate_color_provider_mapping("ui", ui::kUiColorsStart,
+                                       ui::kUiColorsEnd, ui::ColorIdName),
+       generate_color_provider_mapping("chrome", kChromeColorsStart,
+                                       kChromeColorsEnd, &ChromeColorIdName),
+       "}"});
+  if (!color_id_sets.empty()) {
+    LOG(ERROR) << "Unrecognized color set(s) specified for "
+                  "chrome://theme/colors.css: "
+               << base::JoinString(color_id_sets, ",");
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  std::move(callback).Run(
+      base::MakeRefCounted<base::RefCountedString>(std::move(css_string)));
+}
+}  // namespace electron

+ 36 - 0
shell/browser/ui/devtools_ui_theme_data_source.h

@@ -0,0 +1,36 @@
+// Copyright (c) 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_THEME_DATA_SOURCE_H_
+#define ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_THEME_DATA_SOURCE_H_
+
+#include "content/public/browser/url_data_source.h"
+
+namespace electron {
+// A ThemeDataSource implementation that handles devtools://theme/
+// requests.
+class ThemeDataSource : public content::URLDataSource {
+ public:
+  ThemeDataSource() = default;
+  ~ThemeDataSource() override = default;
+
+  ThemeDataSource(const ThemeDataSource&) = delete;
+  ThemeDataSource& operator=(const ThemeDataSource&) = delete;
+
+  std::string GetSource() override;
+
+  void StartDataRequest(const GURL& url,
+                        const content::WebContents::Getter& wc_getter,
+                        GotDataCallback callback) override;
+
+ private:
+  std::string GetMimeType(const GURL& url) override;
+
+  void SendColorsCss(const GURL& url,
+                     const content::WebContents::Getter& wc_getter,
+                     content::URLDataSource::GotDataCallback callback);
+};
+
+}  // namespace electron
+#endif  // ELECTRON_SHELL_BROWSER_UI_DEVTOOLS_UI_THEME_DATA_SOURCE_H_

+ 32 - 0
spec/chromium-spec.ts

@@ -2856,6 +2856,38 @@ describe('chromium features', () => {
       await new Promise((resolve) => { utter.onend = resolve; });
     });
   });
+
+  describe('devtools', () => {
+    it('fetch colors.css', async () => {
+      // <link href="devtools://theme/colors.css?sets=ui,chrome" rel="stylesheet">
+      const w = new BrowserWindow({ show: false });
+      const devtools = new BrowserWindow({ show: false });
+      const devToolsOpened = once(w.webContents, 'devtools-opened');
+
+      w.webContents.setDevToolsWebContents(devtools.webContents);
+      w.webContents.openDevTools();
+      await devToolsOpened;
+      expect(devtools.webContents.getURL().startsWith('devtools://devtools')).to.be.true();
+
+      const result = await devtools.webContents.executeJavaScript(`
+        document.body.querySelector('link[href*=\\'//theme/colors.css\\']')?.getAttribute('href');
+      `);
+      expect(result.startsWith('devtools://theme/colors.css?sets=ui,chrome')).to.be.true();
+      const colorAccentResult = await devtools.webContents.executeJavaScript(`
+        const style = getComputedStyle(document.body);
+        style.getPropertyValue('--color-accent');
+      `);
+      expect(colorAccentResult).to.not.equal('');
+      const colorAppMenuHighlightSeverityLow = await devtools.webContents.executeJavaScript(`
+        style.getPropertyValue('--color-app-menu-highlight-severity-low');
+      `);
+      expect(colorAppMenuHighlightSeverityLow).to.not.equal('');
+      const rgb = await devtools.webContents.executeJavaScript(`
+        style.getPropertyValue('--color-accent-rgb');
+      `);
+      expect(rgb).to.equal('');
+    });
+  });
 });
 
 describe('font fallback', () => {