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