diff --git a/change/react-native-windows-2020-04-16-08-41-54-appearance-module.json b/change/react-native-windows-2020-04-16-08-41-54-appearance-module.json
new file mode 100644
index 00000000000..fe356c2fdee
--- /dev/null
+++ b/change/react-native-windows-2020-04-16-08-41-54-appearance-module.json
@@ -0,0 +1,8 @@
+{
+ "type": "prerelease",
+ "comment": "Implement AppearanceModule",
+ "packageName": "react-native-windows",
+ "email": "ngerlem@microsoft.com",
+ "dependentChangeType": "patch",
+ "date": "2020-04-16T15:41:54.563Z"
+}
\ No newline at end of file
diff --git a/packages/playground/windows/playground-win32.sln b/packages/playground/windows/playground-win32.sln
index 8f15aa41d58..8b30f151e52 100644
--- a/packages/playground/windows/playground-win32.sln
+++ b/packages/playground/windows/playground-win32.sln
@@ -41,6 +41,7 @@ EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
..\..\..\vnext\Chakra\Chakra.vcxitems*{2d5d43d9-cffc-4c40-b4cd-02efb4e2742b}*SharedItemsImports = 4
+ ..\..\..\vnext\Mso\Mso.vcxitems*{2d5d43d9-cffc-4c40-b4cd-02efb4e2742b}*SharedItemsImports = 4
..\..\..\vnext\Shared\Shared.vcxitems*{2d5d43d9-cffc-4c40-b4cd-02efb4e2742b}*SharedItemsImports = 4
..\..\..\vnext\JSI\Shared\JSI.Shared.vcxitems*{a62d504a-16b8-41d2-9f19-e2e86019e5e4}*SharedItemsImports = 4
EndGlobalSection
diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
index 8ca892755b2..2ba8b5f70d5 100644
--- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
+++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
@@ -205,6 +205,7 @@
+
@@ -367,6 +368,7 @@
+
diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
index 3680c005866..3fe649bb226 100644
--- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
+++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
@@ -86,6 +86,9 @@
Modules\Animated
+
+ Modules
+
Modules
@@ -401,6 +404,9 @@
Modules\Animated
+
+ Modules
+
Modules
diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp
index f7632e6f3e9..0a6f50829b8 100644
--- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp
+++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.cpp
@@ -152,6 +152,7 @@ void ReactInstanceWin::Initialize() noexcept {
strongThis->m_appTheme =
std::make_shared(legacyInstance, strongThis->m_uiMessageThread.LoadWithLock());
strongThis->m_i18nInfo = react::uwp::I18nModule::GetI18nInfo();
+ strongThis->m_appearanceListener = Mso::Make(legacyInstance);
}
})
.Then(Queue(), [ this, weakThis = Mso::WeakPtr{this} ]() noexcept {
@@ -198,6 +199,7 @@ void ReactInstanceWin::Initialize() noexcept {
std::move(m_i18nInfo),
std::move(m_appState),
std::move(m_appTheme),
+ std::move(m_appearanceListener),
m_legacyReactInstance);
cxxModules.emplace_back(
diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h
index d8c08a2bff7..0543bf8511d 100644
--- a/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h
+++ b/vnext/Microsoft.ReactNative/ReactHost/ReactInstanceWin.h
@@ -10,6 +10,7 @@
#include
#include
+#include
#include
#include
#include "UwpReactInstanceProxy.h"
@@ -155,6 +156,7 @@ class ReactInstanceWin final : public Mso::ActiveObject m_redboxHandler;
std::shared_ptr m_appTheme;
std::pair m_i18nInfo{};
+ Mso::CntPtr m_appearanceListener;
std::string m_bundleRootPath;
};
diff --git a/vnext/ReactUWP/Base/CoreNativeModules.cpp b/vnext/ReactUWP/Base/CoreNativeModules.cpp
index 90edeccc286..312042ce6b1 100644
--- a/vnext/ReactUWP/Base/CoreNativeModules.cpp
+++ b/vnext/ReactUWP/Base/CoreNativeModules.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -55,6 +56,7 @@ std::vector GetCoreModules(
I18nModule::I18nInfo &&i18nInfo,
std::shared_ptr &&appstate,
std::shared_ptr &&appTheme,
+ Mso::CntPtr &&appearanceListener,
const std::shared_ptr &uwpInstance) noexcept {
// Modules
std::vector modules;
@@ -125,6 +127,13 @@ std::vector GetCoreModules(
},
messageQueue);
+ modules.emplace_back(
+ AppearanceModule::Name,
+ [appearanceListener = std::move(appearanceListener)]() mutable {
+ return std::make_unique(std::move(appearanceListener));
+ },
+ messageQueue);
+
// AsyncStorageModule doesn't work without package identity (it indirectly depends on
// Windows.Storage.StorageFile), so check for package identity before adding it.
modules.emplace_back(
diff --git a/vnext/ReactUWP/Base/CoreNativeModules.h b/vnext/ReactUWP/Base/CoreNativeModules.h
index 82b3bf83612..f5ef3861f65 100644
--- a/vnext/ReactUWP/Base/CoreNativeModules.h
+++ b/vnext/ReactUWP/Base/CoreNativeModules.h
@@ -4,8 +4,10 @@
#pragma once
#include
+#include
#include
#include
+#include
#include
#include
@@ -30,6 +32,7 @@ std::vector GetCoreModules(
I18nModule::I18nInfo &&i18nInfo,
std::shared_ptr &&appstate,
std::shared_ptr &&appTheme,
+ Mso::CntPtr &&appearanceListener,
const std::shared_ptr &uwpInstance) noexcept;
} // namespace react::uwp
diff --git a/vnext/ReactUWP/Base/UwpReactInstance.cpp b/vnext/ReactUWP/Base/UwpReactInstance.cpp
index 436449e495e..bff646a8257 100644
--- a/vnext/ReactUWP/Base/UwpReactInstance.cpp
+++ b/vnext/ReactUWP/Base/UwpReactInstance.cpp
@@ -114,6 +114,7 @@ void UwpReactInstance::Start(const std::shared_ptr &spThis, cons
std::shared_ptr appTheme =
std::make_shared(spThis, m_defaultNativeThread);
std::pair i18nInfo = I18nModule::GetI18nInfo();
+ auto appearanceListener = Mso::Make(spThis);
// TODO: Figure out threading. What thread should this really be on?
m_initThread = std::make_unique();
@@ -124,7 +125,8 @@ void UwpReactInstance::Start(const std::shared_ptr &spThis, cons
settings,
i18nInfo = std::move(i18nInfo),
appstate = std::move(appstate),
- appTheme = std::move(appTheme)]() mutable {
+ appTheme = std::move(appTheme),
+ appearanceListener = std::move(appearanceListener)]() mutable {
// Setup DevSettings based on our own internal structure
auto devSettings(std::make_shared());
devSettings->debugBundlePath = settings.DebugBundlePath;
@@ -203,6 +205,7 @@ void UwpReactInstance::Start(const std::shared_ptr &spThis, cons
std::move(i18nInfo),
std::move(appstate),
std::move(appTheme),
+ std::move(appearanceListener),
spThis);
cxxModules.emplace_back(
diff --git a/vnext/ReactUWP/Modules/AppearanceModule.cpp b/vnext/ReactUWP/Modules/AppearanceModule.cpp
new file mode 100644
index 00000000000..5235c110976
--- /dev/null
+++ b/vnext/ReactUWP/Modules/AppearanceModule.cpp
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#include "pch.h"
+#include "AppearanceModule.h"
+
+#include
+
+using Application = winrt::Windows::UI::Xaml::Application;
+using ApplicationTheme = winrt::Windows::UI::Xaml::ApplicationTheme;
+using UISettings = winrt::Windows::UI::ViewManagement::UISettings;
+
+using Method = facebook::xplat::module::CxxModule::Method;
+
+namespace react::uwp {
+
+AppearanceChangeListener::AppearanceChangeListener(std::weak_ptr &&reactInstance) noexcept
+ : Mso::ActiveObject<>(Mso::DispatchQueue::MainUIQueue()), m_weakReactInstance(std::move(reactInstance)) {
+ // Ensure we're constructed on the UI thread
+ VerifyIsInQueueElseCrash();
+
+ m_currentTheme = Application::Current().RequestedTheme();
+
+ // UISettings will notify us on a background thread regardless of where we construct it or register for events.
+ // Redirect callbacks to the UI thread where we can check app theme.
+ m_revoker = m_uiSettings.ColorValuesChanged(
+ winrt::auto_revoke, [weakThis{Mso::WeakPtr(this)}](const auto & /*sender*/, const auto & /*args*/) noexcept {
+ if (auto strongThis = weakThis.GetStrongPtr()) {
+ strongThis->InvokeInQueueStrong([strongThis]() noexcept { strongThis->OnColorValuesChanged(); });
+ }
+ });
+}
+
+const char *AppearanceChangeListener::GetColorScheme() const noexcept {
+ return ToString(m_currentTheme);
+}
+
+const char *AppearanceChangeListener::ToString(ApplicationTheme theme) noexcept {
+ return theme == ApplicationTheme::Dark ? "dark" : "light";
+}
+
+void AppearanceChangeListener::OnColorValuesChanged() noexcept {
+ auto newTheme = Application::Current().RequestedTheme();
+ if (m_currentTheme != newTheme) {
+ m_currentTheme = newTheme;
+
+ if (auto reactInstance = m_weakReactInstance.lock()) {
+ reactInstance->CallJsFunction(
+ "RCTDeviceEventEmitter", "emit", folly::dynamic::array("appearanceChanged", ToString(m_currentTheme)));
+ }
+ }
+}
+
+AppearanceModule::AppearanceModule(Mso::CntPtr &&appearanceListener) noexcept
+ : m_changeListener(std::move(appearanceListener)) {}
+
+std::string AppearanceModule::getName() {
+ return AppearanceModule::Name;
+}
+
+std::vector AppearanceModule::getMethods() {
+ return {Method(
+ "getColorScheme", [this](folly::dynamic /*args*/) { return m_changeListener->GetColorScheme(); }, SyncTag)};
+}
+
+} // namespace react::uwp
diff --git a/vnext/ReactUWP/Modules/AppearanceModule.h b/vnext/ReactUWP/Modules/AppearanceModule.h
new file mode 100644
index 00000000000..b4bed580b1a
--- /dev/null
+++ b/vnext/ReactUWP/Modules/AppearanceModule.h
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "IReactInstance.h"
+
+namespace react::uwp {
+
+// Listens for the current theme on the UI thread, storing the most recent. Will emit JS events on Appearance change.
+class AppearanceChangeListener final : public Mso::ActiveObject<> {
+ using ApplicationTheme = winrt::Windows::UI::Xaml::ApplicationTheme;
+ using UISettings = winrt::Windows::UI::ViewManagement::UISettings;
+
+ public:
+ AppearanceChangeListener(std::weak_ptr &&reactInstance) noexcept;
+ const char *GetColorScheme() const noexcept;
+
+ private:
+ static const char *ToString(ApplicationTheme theme) noexcept;
+ void OnColorValuesChanged() noexcept;
+
+ UISettings m_uiSettings;
+ UISettings::ColorValuesChanged_revoker m_revoker;
+ std::atomic m_currentTheme;
+ std::weak_ptr m_weakReactInstance;
+};
+
+class AppearanceModule final : public facebook::xplat::module::CxxModule {
+ public:
+ static constexpr const char *Name = "Appearance";
+
+ AppearanceModule(Mso::CntPtr &&appearanceListener) noexcept;
+ std::string getName() override;
+ std::vector getMethods() override;
+
+ private:
+ Mso::CntPtr m_changeListener;
+};
+
+} // namespace react::uwp
diff --git a/vnext/ReactUWP/Modules/ImageViewManagerModule.h b/vnext/ReactUWP/Modules/ImageViewManagerModule.h
index dcecfdd7509..2bf8fbb21d8 100644
--- a/vnext/ReactUWP/Modules/ImageViewManagerModule.h
+++ b/vnext/ReactUWP/Modules/ImageViewManagerModule.h
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+
+#pragma once
#include
namespace facebook {
diff --git a/vnext/ReactUWP/ReactUWP.vcxproj b/vnext/ReactUWP/ReactUWP.vcxproj
index 8b152403c57..03c65e65794 100644
--- a/vnext/ReactUWP/ReactUWP.vcxproj
+++ b/vnext/ReactUWP/ReactUWP.vcxproj
@@ -65,6 +65,7 @@
+
@@ -77,6 +78,8 @@
Use
+ pch.h
+ pch.h
false
true