Browse Source

feat: `nativeTheme.themeSource` and a few nativeTheme fixes (#20486)

* feat: add nativeTheme.themeSource to allow apps to override Chromiums theme choice (#19960)

* feat: add nativeTheme.shouldUseDarkColorsOverride to allow apps to override Chromiums theme choice

* spec: add tests for shouldUseDarkColorsOverride

* chore: add missing forward declarations

* refactor: rename overrideShouldUseDarkColors to themeSource

* chore: only run appLevelAppearance specs on Mojave and up

* chore: update patch with more info and no define

* Update spec-main/api-native-theme-spec.ts

Co-Authored-By: Jeremy Apthorp <[email protected]>

* Update api-native-theme-spec.ts

* Update api-native-theme-spec.ts

* Update api-native-theme-spec.ts

* fix: don't expose nativeTheme in the renderer process (#20139)

Exposing these in the renderer didn't make sense as they weren't backed
by the same instance / value store.  This API should be browser only
especially now that we have nativeTheme.themeSource.  Exposing in
//common was a mistake from the beginning.

* fix: emit updated on NativeTheme on the UI thread to avoid DCHECK (#20137)

* fix: emit updated on NativeTheme on the UI thread to avoid DCHECK

* Update atom_api_native_theme.cc

* spec: wait a few ticks for async events to emit so that test events do not leak into each other

* chore: add SetGTKDarkThemeEnabled(enabled) internal helper to allow dynamic theme selection on linux (#19964)

This is just a after-creation setter for the `darkTheme` constructor option.  This is delibrately
a method and not a property as there is no getter.

* spec: remove leftover .only
Samuel Attard 5 years ago
parent
commit
0a9b201c34

+ 33 - 2
docs/api/native-theme.md

@@ -2,7 +2,7 @@
 
 > Read and respond to changes in Chromium's native color theme.
 
-Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process)
+Process: [Main](../glossary.md#main-process)
 
 ## Events
 
@@ -22,7 +22,38 @@ The `nativeTheme` module has the following properties:
 ### `nativeTheme.shouldUseDarkColors` _Readonly_
 
 A `Boolean` for if the OS / Chromium currently has a dark mode enabled or is
-being instructed to show a dark-style UI.
+being instructed to show a dark-style UI.  If you want to modify this value you
+should use `themeSource` below.
+
+### `nativeTheme.themeSource`
+
+A `String` property that can be `system`, `light` or `dark`.  It is used to override and supercede
+the value that Chromium has chosen to use internally.
+
+Setting this property to `system` will remove the override and
+everything will be reset to the OS default.  By default `themeSource` is `system`.
+
+Settings this property to `dark` will have the following effects:
+* `nativeTheme.shouldUseDarkColors` will be `true` when accessed
+* Any UI Electron renders on Linux and Windows including context menus, devtools, etc. will use the dark UI.
+* Any UI the OS renders on macOS including menus, window frames, etc. will use the dark UI.
+* The [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS query will match `dark` mode.
+* The `updated` event will be emitted
+
+Settings this property to `light` will have the following effects:
+* `nativeTheme.shouldUseDarkColors` will be `false` when accessed
+* Any UI Electron renders on Linux and Windows including context menus, devtools, etc. will use the light UI.
+* Any UI the OS renders on macOS including menus, window frames, etc. will use the light UI.
+* The [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS query will match `light` mode.
+* The `updated` event will be emitted
+
+The usage of this property should align with a classic "dark mode" state machine in your application
+where the user has three options.
+* `Follow OS` --> `themeSource = 'system'`
+* `Dark Mode` --> `themeSource = 'dark'`
+* `Light Mode` --> `themeSource = 'light'`
+
+Your application should then always use `shouldUseDarkColors` to determine what CSS to apply.
 
 ### `nativeTheme.shouldUseHighContrastColors` _macOS_ _Windows_ _Readonly_
 

+ 1 - 4
filenames.auto.gni

@@ -132,7 +132,6 @@ auto_filenames = {
     "lib/common/api/deprecate.ts",
     "lib/common/api/module-list.js",
     "lib/common/api/native-image.js",
-    "lib/common/api/native-theme.ts",
     "lib/common/api/shell.js",
     "lib/common/buffer-utils.ts",
     "lib/common/clipboard-utils.ts",
@@ -223,6 +222,7 @@ auto_filenames = {
     "lib/browser/api/menu-utils.js",
     "lib/browser/api/menu.js",
     "lib/browser/api/module-list.js",
+    "lib/browser/api/native-theme.ts",
     "lib/browser/api/net-log.js",
     "lib/browser/api/net.js",
     "lib/browser/api/notification.js",
@@ -264,7 +264,6 @@ auto_filenames = {
     "lib/common/api/exports/electron.js",
     "lib/common/api/module-list.js",
     "lib/common/api/native-image.js",
-    "lib/common/api/native-theme.ts",
     "lib/common/api/shell.js",
     "lib/common/buffer-utils.ts",
     "lib/common/clipboard-utils.ts",
@@ -290,7 +289,6 @@ auto_filenames = {
     "lib/common/api/exports/electron.js",
     "lib/common/api/module-list.js",
     "lib/common/api/native-image.js",
-    "lib/common/api/native-theme.ts",
     "lib/common/api/shell.js",
     "lib/common/buffer-utils.ts",
     "lib/common/clipboard-utils.ts",
@@ -341,7 +339,6 @@ auto_filenames = {
     "lib/common/api/exports/electron.js",
     "lib/common/api/module-list.js",
     "lib/common/api/native-image.js",
-    "lib/common/api/native-theme.ts",
     "lib/common/api/shell.js",
     "lib/common/buffer-utils.ts",
     "lib/common/clipboard-utils.ts",

+ 3 - 2
filenames.gni

@@ -65,6 +65,9 @@ filenames = {
     "shell/browser/api/atom_api_menu_mac.mm",
     "shell/browser/api/atom_api_menu_views.cc",
     "shell/browser/api/atom_api_menu_views.h",
+    "shell/browser/api/atom_api_native_theme.cc",
+    "shell/browser/api/atom_api_native_theme.h",
+    "shell/browser/api/atom_api_native_theme_mac.mm",
     "shell/browser/api/atom_api_net.cc",
     "shell/browser/api/atom_api_net.h",
     "shell/browser/api/atom_api_net_log.cc",
@@ -423,8 +426,6 @@ filenames = {
     "shell/common/api/atom_api_native_image.cc",
     "shell/common/api/atom_api_native_image.h",
     "shell/common/api/atom_api_native_image_mac.mm",
-    "shell/common/api/atom_api_native_theme.cc",
-    "shell/common/api/atom_api_native_theme.h",
     "shell/common/api/atom_api_shell.cc",
     "shell/common/api/atom_api_v8_util.cc",
     "shell/common/api/electron_bindings.cc",

+ 1 - 0
lib/browser/api/module-list.js

@@ -18,6 +18,7 @@ module.exports = [
   { name: 'inAppPurchase', loader: () => require('./in-app-purchase') },
   { name: 'Menu', loader: () => require('./menu') },
   { name: 'MenuItem', loader: () => require('./menu-item') },
+  { name: 'nativeTheme', loader: () => require('./native-theme') },
   { name: 'net', loader: () => require('./net') },
   { name: 'netLog', loader: () => require('./net-log') },
   { name: 'Notification', loader: () => require('./notification') },

+ 0 - 0
lib/common/api/native-theme.ts → lib/browser/api/native-theme.ts


+ 0 - 1
lib/common/api/module-list.js

@@ -4,7 +4,6 @@
 module.exports = [
   { name: 'clipboard', loader: () => require('./clipboard') },
   { name: 'nativeImage', loader: () => require('./native-image') },
-  { name: 'nativeTheme', loader: () => require('./native-theme') },
   { name: 'shell', loader: () => require('./shell') },
   // The internal modules, invisible unless you know their names.
   { name: 'deprecate', loader: () => require('./deprecate'), private: true }

+ 2 - 1
package.json

@@ -14,6 +14,7 @@
     "@types/fs-extra": "^5.0.5",
     "@types/mocha": "^5.2.6",
     "@types/node": "^12.0.10",
+    "@types/semver": "^6.0.1",
     "@types/split": "^1.0.0",
     "@types/webpack": "^4.4.32",
     "@types/webpack-env": "^1.13.9",
@@ -125,4 +126,4 @@
       "git add filenames.auto.gni"
     ]
   }
-}
+}

+ 1 - 0
patches/chromium/.patches

@@ -78,3 +78,4 @@ ssl_security_state_tab_helper.patch
 revert_cleanup_remove_menu_subtitles_sublabels.patch
 expose_setuseragent_on_networkcontext.patch
 net_avoid_vector_const_elements.patch
+feat_add_set_theme_source_to_allow_apps_to.patch

+ 89 - 0
patches/chromium/feat_add_set_theme_source_to_allow_apps_to.patch

@@ -0,0 +1,89 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <[email protected]>
+Date: Mon, 26 Aug 2019 14:32:41 -0700
+Subject: feat: add set_theme_source to allow apps to override chromiums
+ internal theme choice
+
+This patch is required as Chromium doesn't currently let folks using
+//ui override the theme choice in NativeTheme.  It defaults to
+respecting the OS theme choice and some apps don't always want to do
+that.  With this patch we can override the theme value that Chromium
+uses internally for things like menus and devtools.
+
+We can remove this patch once it has in some shape been upstreamed.
+
+diff --git a/ui/native_theme/native_theme.cc b/ui/native_theme/native_theme.cc
+index 2370d15332c8c6c7dc7e3403b38891c885704d9f..171214379437f319d3feccc289a5d91e74b77f9e 100644
+--- a/ui/native_theme/native_theme.cc
++++ b/ui/native_theme/native_theme.cc
+@@ -40,6 +40,8 @@ NativeTheme::NativeTheme()
+ NativeTheme::~NativeTheme() = default;
+ 
+ bool NativeTheme::ShouldUseDarkColors() const {
++  if (theme_source() == ThemeSource::kForcedLight) return false;
++  if (theme_source() == ThemeSource::kForcedDark) return true;
+   return should_use_dark_colors_;
+ }
+ 
+diff --git a/ui/native_theme/native_theme.h b/ui/native_theme/native_theme.h
+index 70389e0245993faa2c17e9deefeb000280ef2368..cef1c0d4706e7e720a4004ca54765a39fc29c5e8 100644
+--- a/ui/native_theme/native_theme.h
++++ b/ui/native_theme/native_theme.h
+@@ -429,6 +429,22 @@ class NATIVE_THEME_EXPORT NativeTheme {
+       ColorId color_id,
+       ColorScheme color_scheme = ColorScheme::kDefault) const = 0;
+ 
++  enum ThemeSource {
++    kSystem,
++    kForcedDark,
++    kForcedLight,
++  };
++
++  ThemeSource theme_source() const {
++    return theme_source_;
++  }
++
++  void set_theme_source(ThemeSource theme_source) {
++    bool original = ShouldUseDarkColors();
++    theme_source_ = theme_source;
++    if (ShouldUseDarkColors() != original) NotifyObservers();
++  }
++
+   // Returns a shared instance of the native theme that should be used for web
+   // rendering. Do not use it in a normal application context (i.e. browser).
+   // The returned object should not be deleted by the caller. This function is
+@@ -547,6 +563,8 @@ class NATIVE_THEME_EXPORT NativeTheme {
+   PreferredColorScheme preferred_color_scheme_ =
+       PreferredColorScheme::kNoPreference;
+ 
++  ThemeSource theme_source_ = ThemeSource::kSystem;
++
+   DISALLOW_COPY_AND_ASSIGN(NativeTheme);
+ };
+ 
+diff --git a/ui/native_theme/native_theme_dark_aura.cc b/ui/native_theme/native_theme_dark_aura.cc
+index a8fbfee3b13672902aac05fd5a65fa8ee81f9f7e..1be6369acf0b7c02a6f862636c2b2de1fbf8cb5a 100644
+--- a/ui/native_theme/native_theme_dark_aura.cc
++++ b/ui/native_theme/native_theme_dark_aura.cc
+@@ -20,6 +20,8 @@ SkColor NativeThemeDarkAura::GetSystemColor(ColorId color_id,
+ }
+ 
+ bool NativeThemeDarkAura::ShouldUseDarkColors() const {
++  if (theme_source() == ThemeSource::kForcedLight) return false;
++  if (theme_source() == ThemeSource::kForcedDark) return true;
+   return true;
+ }
+ 
+diff --git a/ui/native_theme/native_theme_win.cc b/ui/native_theme/native_theme_win.cc
+index 3003643bfb78cec2f5e84fc9e1471e1ef54aae41..06f2cbc84401958d49445f4ce6acb1b2fef0aa04 100644
+--- a/ui/native_theme/native_theme_win.cc
++++ b/ui/native_theme/native_theme_win.cc
+@@ -611,6 +611,8 @@ bool NativeThemeWin::ShouldUseDarkColors() const {
+   // ...unless --force-dark-mode was specified in which case caveat emptor.
+   if (UsesHighContrastColors() && !IsForcedDarkMode())
+     return false;
++  if (theme_source() == ThemeSource::kForcedLight) return false;
++  if (theme_source() == ThemeSource::kForcedDark) return true;
+   return NativeTheme::ShouldUseDarkColors();
+ }
+ 

+ 68 - 2
shell/common/api/atom_api_native_theme.cc → shell/browser/api/atom_api_native_theme.cc

@@ -2,8 +2,13 @@
 // Use of this source code is governed by the MIT license that can be
 // found in the LICENSE file.
 
-#include "shell/common/api/atom_api_native_theme.h"
+#include "shell/browser/api/atom_api_native_theme.h"
 
+#include <string>
+
+#include "base/task/post_task.h"
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
 #include "native_mate/dictionary.h"
 #include "native_mate/object_template_builder.h"
 #include "shell/common/node_includes.h"
@@ -24,10 +29,31 @@ NativeTheme::~NativeTheme() {
   theme_->RemoveObserver(this);
 }
 
-void NativeTheme::OnNativeThemeUpdated(ui::NativeTheme* theme) {
+void NativeTheme::OnNativeThemeUpdatedOnUI() {
   Emit("updated");
 }
 
+void NativeTheme::OnNativeThemeUpdated(ui::NativeTheme* theme) {
+  base::PostTaskWithTraits(
+      FROM_HERE, {content::BrowserThread::UI},
+      base::BindOnce(&NativeTheme::OnNativeThemeUpdatedOnUI,
+                     base::Unretained(this)));
+}
+
+void NativeTheme::SetThemeSource(ui::NativeTheme::ThemeSource override) {
+  theme_->set_theme_source(override);
+#if defined(OS_MACOSX)
+  // Update the macOS appearance setting for this new override value
+  UpdateMacOSAppearanceForOverrideValue(override);
+#endif
+  // TODO(MarshallOfSound): Update all existing browsers windows to use GTK dark
+  // theme
+}
+
+ui::NativeTheme::ThemeSource NativeTheme::GetThemeSource() const {
+  return theme_->theme_source();
+}
+
 bool NativeTheme::ShouldUseDarkColors() {
   return theme_->ShouldUseDarkColors();
 }
@@ -68,6 +94,8 @@ void NativeTheme::BuildPrototype(v8::Isolate* isolate,
   prototype->SetClassName(mate::StringToV8(isolate, "NativeTheme"));
   mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
       .SetProperty("shouldUseDarkColors", &NativeTheme::ShouldUseDarkColors)
+      .SetProperty("themeSource", &NativeTheme::GetThemeSource,
+                   &NativeTheme::SetThemeSource)
       .SetProperty("shouldUseHighContrastColors",
                    &NativeTheme::ShouldUseHighContrastColors)
       .SetProperty("shouldUseInvertedColorScheme",
@@ -94,4 +122,42 @@ void Initialize(v8::Local<v8::Object> exports,
 
 }  // namespace
 
+namespace mate {
+
+v8::Local<v8::Value> Converter<ui::NativeTheme::ThemeSource>::ToV8(
+    v8::Isolate* isolate,
+    const ui::NativeTheme::ThemeSource& val) {
+  switch (val) {
+    case ui::NativeTheme::ThemeSource::kForcedDark:
+      return mate::ConvertToV8(isolate, "dark");
+    case ui::NativeTheme::ThemeSource::kForcedLight:
+      return mate::ConvertToV8(isolate, "light");
+    case ui::NativeTheme::ThemeSource::kSystem:
+    default:
+      return mate::ConvertToV8(isolate, "system");
+  }
+}
+
+bool Converter<ui::NativeTheme::ThemeSource>::FromV8(
+    v8::Isolate* isolate,
+    v8::Local<v8::Value> val,
+    ui::NativeTheme::ThemeSource* out) {
+  std::string theme_source;
+  if (mate::ConvertFromV8(isolate, val, &theme_source)) {
+    if (theme_source == "dark") {
+      *out = ui::NativeTheme::ThemeSource::kForcedDark;
+    } else if (theme_source == "light") {
+      *out = ui::NativeTheme::ThemeSource::kForcedLight;
+    } else if (theme_source == "system") {
+      *out = ui::NativeTheme::ThemeSource::kSystem;
+    } else {
+      return false;
+    }
+    return true;
+  }
+  return false;
+}
+
+}  // namespace mate
+
 NODE_LINKED_MODULE_CONTEXT_AWARE(atom_common_native_theme, Initialize)

+ 24 - 3
shell/common/api/atom_api_native_theme.h → shell/browser/api/atom_api_native_theme.h

@@ -2,11 +2,12 @@
 // Use of this source code is governed by the MIT license that can be
 // found in the LICENSE file.
 
-#ifndef SHELL_COMMON_API_ATOM_API_NATIVE_THEME_H_
-#define SHELL_COMMON_API_ATOM_API_NATIVE_THEME_H_
+#ifndef SHELL_BROWSER_API_ATOM_API_NATIVE_THEME_H_
+#define SHELL_BROWSER_API_ATOM_API_NATIVE_THEME_H_
 
 #include "native_mate/handle.h"
 #include "shell/browser/api/event_emitter.h"
+#include "ui/native_theme/native_theme.h"
 #include "ui/native_theme/native_theme_observer.h"
 
 namespace electron {
@@ -25,12 +26,19 @@ class NativeTheme : public mate::EventEmitter<NativeTheme>,
   NativeTheme(v8::Isolate* isolate, ui::NativeTheme* theme);
   ~NativeTheme() override;
 
+  void SetThemeSource(ui::NativeTheme::ThemeSource override);
+#if defined(OS_MACOSX)
+  void UpdateMacOSAppearanceForOverrideValue(
+      ui::NativeTheme::ThemeSource override);
+#endif
+  ui::NativeTheme::ThemeSource GetThemeSource() const;
   bool ShouldUseDarkColors();
   bool ShouldUseHighContrastColors();
   bool ShouldUseInvertedColorScheme();
 
   // ui::NativeThemeObserver:
   void OnNativeThemeUpdated(ui::NativeTheme* theme) override;
+  void OnNativeThemeUpdatedOnUI();
 
  private:
   ui::NativeTheme* theme_;
@@ -42,4 +50,17 @@ class NativeTheme : public mate::EventEmitter<NativeTheme>,
 
 }  // namespace electron
 
-#endif  // SHELL_COMMON_API_ATOM_API_NATIVE_THEME_H_
+namespace mate {
+
+template <>
+struct Converter<ui::NativeTheme::ThemeSource> {
+  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
+                                   const ui::NativeTheme::ThemeSource& val);
+  static bool FromV8(v8::Isolate* isolate,
+                     v8::Local<v8::Value> val,
+                     ui::NativeTheme::ThemeSource* out);
+};
+
+}  // namespace mate
+
+#endif  // SHELL_BROWSER_API_ATOM_API_NATIVE_THEME_H_

+ 37 - 0
shell/browser/api/atom_api_native_theme_mac.mm

@@ -0,0 +1,37 @@
+// Copyright (c) 2019 Slack Technologies, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/api/atom_api_native_theme.h"
+
+#include "base/mac/sdk_forward_declarations.h"
+#include "shell/browser/mac/atom_application.h"
+
+namespace electron {
+
+namespace api {
+
+void NativeTheme::UpdateMacOSAppearanceForOverrideValue(
+    ui::NativeTheme::ThemeSource override) {
+  if (@available(macOS 10.14, *)) {
+    NSAppearance* new_appearance;
+    switch (override) {
+      case ui::NativeTheme::ThemeSource::kForcedDark:
+        new_appearance =
+            [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
+        break;
+      case ui::NativeTheme::ThemeSource::kForcedLight:
+        new_appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
+        break;
+      case ui::NativeTheme::ThemeSource::kSystem:
+      default:
+        new_appearance = nil;
+        break;
+    }
+    [[NSApplication sharedApplication] setAppearance:new_appearance];
+  }
+}
+
+}  // namespace api
+
+}  // namespace electron

+ 4 - 0
shell/browser/api/atom_api_top_level_window.cc

@@ -866,6 +866,10 @@ void TopLevelWindow::CloseFilePreview() {
   window_->CloseFilePreview();
 }
 
+void TopLevelWindow::SetGTKDarkThemeEnabled(bool use_dark_theme) {
+  window_->SetGTKDarkThemeEnabled(use_dark_theme);
+}
+
 v8::Local<v8::Value> TopLevelWindow::GetContentView() const {
   if (content_view_.IsEmpty())
     return v8::Null(isolate());

+ 1 - 0
shell/browser/api/atom_api_top_level_window.h

@@ -198,6 +198,7 @@ class TopLevelWindow : public mate::TrackableObject<TopLevelWindow>,
   void SetAspectRatio(double aspect_ratio, mate::Arguments* args);
   void PreviewFile(const std::string& path, mate::Arguments* args);
   void CloseFilePreview();
+  void SetGTKDarkThemeEnabled(bool use_dark_theme);
 
   // Public getters of NativeWindow.
   v8::Local<v8::Value> GetContentView() const;

+ 2 - 0
shell/browser/native_window.h

@@ -218,6 +218,8 @@ class NativeWindow : public base::SupportsUserData,
                            const std::string& display_name);
   virtual void CloseFilePreview();
 
+  virtual void SetGTKDarkThemeEnabled(bool use_dark_theme) = 0;
+
   // Converts between content bounds and window bounds.
   virtual gfx::Rect ContentBoundsToWindowBounds(
       const gfx::Rect& bounds) const = 0;

+ 1 - 0
shell/browser/native_window_mac.h

@@ -135,6 +135,7 @@ class NativeWindowMac : public NativeWindow {
       const std::vector<mate::PersistentDictionary>& items) override;
   void RefreshTouchBarItem(const std::string& item_id) override;
   void SetEscapeTouchBarItem(const mate::PersistentDictionary& item) override;
+  void SetGTKDarkThemeEnabled(bool use_dark_theme) override {}
 
   gfx::Rect ContentBoundsToWindowBounds(const gfx::Rect& bounds) const override;
   gfx::Rect WindowBoundsToContentBounds(const gfx::Rect& bounds) const override;

+ 20 - 6
shell/browser/native_window_views.cc

@@ -213,12 +213,7 @@ NativeWindowViews::NativeWindowViews(const mate::Dictionary& options,
   // Set _GTK_THEME_VARIANT to dark if we have "dark-theme" option set.
   bool use_dark_theme = false;
   if (options.Get(options::kDarkTheme, &use_dark_theme) && use_dark_theme) {
-    XDisplay* xdisplay = gfx::GetXDisplay();
-    XChangeProperty(xdisplay, GetAcceleratedWidget(),
-                    XInternAtom(xdisplay, "_GTK_THEME_VARIANT", x11::False),
-                    XInternAtom(xdisplay, "UTF8_STRING", x11::False), 8,
-                    PropModeReplace,
-                    reinterpret_cast<const unsigned char*>("dark"), 4);
+    SetGTKDarkThemeEnabled(use_dark_theme);
   }
 
   // Before the window is mapped the SetWMSpecState can not work, so we have
@@ -329,6 +324,25 @@ NativeWindowViews::~NativeWindowViews() {
 #endif
 }
 
+void NativeWindowViews::SetGTKDarkThemeEnabled(bool use_dark_theme) {
+#if defined(USE_X11)
+  XDisplay* xdisplay = gfx::GetXDisplay();
+  if (use_dark_theme) {
+    XChangeProperty(xdisplay, GetAcceleratedWidget(),
+                    XInternAtom(xdisplay, "_GTK_THEME_VARIANT", x11::False),
+                    XInternAtom(xdisplay, "UTF8_STRING", x11::False), 8,
+                    PropModeReplace,
+                    reinterpret_cast<const unsigned char*>("dark"), 4);
+  } else {
+    XChangeProperty(xdisplay, GetAcceleratedWidget(),
+                    XInternAtom(xdisplay, "_GTK_THEME_VARIANT", x11::False),
+                    XInternAtom(xdisplay, "UTF8_STRING", x11::False), 8,
+                    PropModeReplace,
+                    reinterpret_cast<const unsigned char*>("light"), 5);
+  }
+#endif
+}
+
 void NativeWindowViews::SetContentView(views::View* view) {
   if (content_view()) {
     root_view_->RemoveChildView(content_view());

+ 2 - 0
shell/browser/native_window_views.h

@@ -124,6 +124,8 @@ class NativeWindowViews : public NativeWindow,
 
   bool IsVisibleOnAllWorkspaces() override;
 
+  void SetGTKDarkThemeEnabled(bool use_dark_theme) override;
+
   gfx::AcceleratedWidget GetAcceleratedWidget() const override;
   NativeWindowHandle GetNativeWindowHandle() const override;
 

+ 57 - 1
spec-main/api-native-theme-spec.ts

@@ -1,5 +1,10 @@
 import { expect } from 'chai'
-import { nativeTheme } from 'electron'
+import { nativeTheme, systemPreferences } from 'electron'
+import * as os from 'os'
+import * as semver from 'semver'
+
+import { delay, ifdescribe } from './spec-helpers'
+import { emittedOnce } from './events-helpers';
 
 describe('nativeTheme module', () => {
   describe('nativeTheme.shouldUseDarkColors', () => {
@@ -8,6 +13,57 @@ describe('nativeTheme module', () => {
     })
   })
 
+  describe('nativeTheme.themeSource', () => {
+    afterEach(async () => {
+      nativeTheme.themeSource = 'system'
+      // Wait for any pending events to emit
+      await delay(20)
+    })
+
+    it('is system by default', () => {
+      expect(nativeTheme.themeSource).to.equal('system')
+    })
+
+    it('should override the value of shouldUseDarkColors', () => {
+      nativeTheme.themeSource = 'dark'
+      expect(nativeTheme.shouldUseDarkColors).to.equal(true)
+      nativeTheme.themeSource = 'light'
+      expect(nativeTheme.shouldUseDarkColors).to.equal(false)
+    })
+
+    it('should emit the "updated" event when it is set and the resulting "shouldUseDarkColors" value changes', async () => {
+      let updatedEmitted = emittedOnce(nativeTheme, 'updated')
+      nativeTheme.themeSource = 'dark'
+      await updatedEmitted
+      updatedEmitted = emittedOnce(nativeTheme, 'updated')
+      nativeTheme.themeSource = 'light'
+      await updatedEmitted
+    })
+
+    it('should not emit the "updated" event when it is set and the resulting "shouldUseDarkColors" value is the same', async () => {
+      nativeTheme.themeSource = 'dark'
+      // Wait a few ticks to allow an async events to flush
+      await delay(20)
+      let called = false
+      nativeTheme.once('updated', () => {
+        called = true
+      })
+      nativeTheme.themeSource = 'dark'
+      // Wait a few ticks to allow an async events to flush
+      await delay(20)
+      expect(called).to.equal(false)
+    })
+
+    ifdescribe(process.platform === 'darwin' && semver.gte(os.release(), '18.0.0'))('on macOS 10.14', () => {
+      it('should update appLevelAppearance when set', () => {
+        nativeTheme.themeSource = 'dark'
+        expect(systemPreferences.appLevelAppearance).to.equal('dark')
+        nativeTheme.themeSource = 'light'
+        expect(systemPreferences.appLevelAppearance).to.equal('light')
+      })
+    })
+  })
+
   describe('nativeTheme.shouldUseInvertedColorScheme', () => {
     it('returns a boolean', () => {
       expect(nativeTheme.shouldUseInvertedColorScheme).to.be.a('boolean')

+ 3 - 1
spec-main/spec-helpers.ts

@@ -1,2 +1,4 @@
 export const ifit = (condition: boolean) => (condition ? it : it.skip)
-export const ifdescribe = (condition: boolean) => (condition ? describe : describe.skip)
+export const ifdescribe = (condition: boolean) => (condition ? describe : describe.skip)
+
+export const delay = (time: number) => new Promise(r => setTimeout(r, time))

+ 5 - 0
yarn.lock

@@ -239,6 +239,11 @@
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
 
+"@types/semver@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.1.tgz#a984b405c702fa5a7ec6abc56b37f2ba35ef5af6"
+  integrity sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==
+
 "@types/serve-static@*":
   version "1.13.2"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"