From 45a36cf83a2837afe40e52b9243db635cc8af0d5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 12 Jan 2023 09:43:40 -0600 Subject: [PATCH] Enable switching app theme based off of OS theme (#14497) This is basically just like #14064, but with the `theme` instead. If you define a pair of `theme` names: ```json "theme": { "dark": "light", "light": "dark" }, ``` then the Terminal will use the one relevant for the current OS theme. This cooperates with #14064, who sets the `scheme` based on the app's theme. This was spec'd as a part of #3327 / #12530, but never promoted to its own issue. Gif below. --- .../GlobalAppearanceViewModel.cpp | 2 +- .../CascadiaSettings.cpp | 34 ++++++++++--- .../GlobalAppSettings.cpp | 18 ++++++- .../GlobalAppSettings.idl | 2 +- .../TerminalSettingsModel/MTSMSettings.h | 2 +- ...crosoft.Terminal.Settings.ModelLib.vcxproj | 1 + .../TerminalSettingsModel/SettingsUtils.h | 6 +++ .../TerminalSettings.cpp | 23 +++++---- src/cascadia/TerminalSettingsModel/Theme.cpp | 42 +++++++++++++++ src/cascadia/TerminalSettingsModel/Theme.h | 51 +++++++++++++++++++ src/cascadia/TerminalSettingsModel/Theme.idl | 10 ++++ 11 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 src/cascadia/TerminalSettingsModel/SettingsUtils.h diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp index c57db748c28..c9e9c51ec73 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.cpp @@ -218,7 +218,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { if (const auto& theme{ tag.try_as() }) { - _GlobalSettings.Theme(theme.Name()); + _GlobalSettings.Theme(Model::ThemePair{ theme.Name() }); } } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index c9aa9ff8e81..a88f06915e9 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -1167,7 +1167,8 @@ void CascadiaSettings::ExportFile(winrt::hstring path, winrt::hstring content) void CascadiaSettings::_validateThemeExists() { - if (_globals->Themes().Size() == 0) + const auto& themes{ _globals->Themes() }; + if (themes.Size() == 0) { // We didn't even load the default themes. This should only be possible // if the defaults.json didn't include any themes, or if no @@ -1178,14 +1179,33 @@ void CascadiaSettings::_validateThemeExists() auto newTheme = winrt::make_self(); newTheme->Name(L"system"); _globals->AddTheme(*newTheme); - _globals->Theme(L"system"); + _globals->Theme(Model::ThemePair{ L"system" }); } - if (!_globals->Themes().HasKey(_globals->Theme())) + const auto& theme{ _globals->Theme() }; + if (theme.DarkName() == theme.LightName()) { - _warnings.Append(SettingsLoadWarnings::UnknownTheme); - - // safely fall back to system as the theme. - _globals->Theme(L"system"); + // Only one theme. We'll treat it as such. + if (!themes.HasKey(theme.DarkName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + // safely fall back to system as the theme. + _globals->Theme(*winrt::make_self(L"system")); + } + } + else + { + // Two different themes. Check each separately, and fall back to a + // reasonable default contextually + if (!themes.HasKey(theme.LightName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + theme.LightName(L"light"); + } + if (!themes.HasKey(theme.DarkName())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + theme.DarkName(L"dark"); + } } } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 64ddea29f06..e87fb84c54c 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -6,6 +6,7 @@ #include "../../types/inc/Utils.hpp" #include "JsonUtils.h" #include "KeyChordSerialization.h" +#include "SettingsUtils.h" #include "GlobalAppSettings.g.cpp" @@ -212,7 +213,22 @@ Json::Value GlobalAppSettings::ToJson() const winrt::Microsoft::Terminal::Settings::Model::Theme GlobalAppSettings::CurrentTheme() noexcept { - return _themes.TryLookup(Theme()); + auto requestedTheme = IsSystemInDarkTheme() ? + winrt::Windows::UI::Xaml::ElementTheme::Dark : + winrt::Windows::UI::Xaml::ElementTheme::Light; + + switch (requestedTheme) + { + case winrt::Windows::UI::Xaml::ElementTheme::Light: + return _themes.TryLookup(Theme().LightName()); + + case winrt::Windows::UI::Xaml::ElementTheme::Dark: + return _themes.TryLookup(Theme().DarkName()); + + case winrt::Windows::UI::Xaml::ElementTheme::Default: + default: + return nullptr; + } } void GlobalAppSettings::AddTheme(const Model::Theme& theme) diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 06931641e41..4ee2b0bf9e6 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -106,7 +106,7 @@ namespace Microsoft.Terminal.Settings.Model Windows.Foundation.Collections.IMapView Themes(); void AddTheme(Theme theme); - INHERITABLE_SETTING(String, Theme); + INHERITABLE_SETTING(ThemePair, Theme); Theme CurrentTheme { get; }; } } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 37029ec71ab..cb0ed03e3f1 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -34,7 +34,7 @@ Author(s): X(Model::NewTabPosition, NewTabPosition, "newTabPosition", Model::NewTabPosition::AfterLastTab) \ X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ - X(hstring, Theme, "theme") \ + X(Model::ThemePair, Theme, "theme") \ X(hstring, Language, "language") \ X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index 06d89dcf6a6..f102ec0cca6 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -115,6 +115,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/SettingsUtils.h b/src/cascadia/TerminalSettingsModel/SettingsUtils.h new file mode 100644 index 00000000000..f01d628c40c --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SettingsUtils.h @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +bool IsSystemInDarkTheme(); diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp index 39510e1b951..8474aa345d2 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/TerminalSettings.cpp @@ -7,10 +7,21 @@ #include "TerminalSettings.g.cpp" #include "TerminalSettingsCreateResult.g.cpp" +#include "SettingsUtils.h" using namespace winrt::Microsoft::Terminal::Control; using namespace Microsoft::Console::Utils; +// I'm not even joking, this is the recommended way to do this: +// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes#know-when-dark-mode-is-enabled +bool IsSystemInDarkTheme() +{ + static auto isColorLight = [](const winrt::Windows::UI::Color& clr) -> bool { + return (((5 * clr.G) + (2 * clr.R) + clr.B) > (8 * 128)); + }; + return isColorLight(winrt::Windows::UI::ViewManagement::UISettings().GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Foreground)); +}; + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { static std::tuple ConvertConvergedAlignment(ConvergedAlignment alignment) @@ -183,16 +194,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation return settingsPair; } - // I'm not even joking, this is the recommended way to do this: - // https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes#know-when-dark-mode-is-enabled - bool _isSystemInDarkTheme() - { - static auto isColorLight = [](const Windows::UI::Color& clr) -> bool { - return (((5 * clr.G) + (2 * clr.R) + clr.B) > (8 * 128)); - }; - return isColorLight(Windows::UI::ViewManagement::UISettings().GetColorValue(Windows::UI::ViewManagement::UIColorType::Foreground)); - } - void TerminalSettings::_ApplyAppearanceSettings(const IAppearanceConfig& appearance, const Windows::Foundation::Collections::IMapView& schemes, const winrt::Microsoft::Terminal::Settings::Model::Theme currentTheme) @@ -203,7 +204,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto requestedTheme = currentTheme.RequestedTheme(); if (requestedTheme == winrt::Windows::UI::Xaml::ElementTheme::Default) { - requestedTheme = _isSystemInDarkTheme() ? + requestedTheme = IsSystemInDarkTheme() ? winrt::Windows::UI::Xaml::ElementTheme::Dark : winrt::Windows::UI::Xaml::ElementTheme::Light; } diff --git a/src/cascadia/TerminalSettingsModel/Theme.cpp b/src/cascadia/TerminalSettingsModel/Theme.cpp index f0d2352f6da..65d4e5dcc54 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.cpp +++ b/src/cascadia/TerminalSettingsModel/Theme.cpp @@ -13,6 +13,7 @@ #include "WindowTheme.g.cpp" #include "TabRowTheme.g.cpp" #include "TabTheme.g.cpp" +#include "ThemePair.g.cpp" #include "Theme.g.cpp" using namespace ::Microsoft::Console; @@ -27,6 +28,8 @@ namespace winrt } static constexpr std::string_view NameKey{ "name" }; +static constexpr std::string_view LightNameKey{ "light" }; +static constexpr std::string_view DarkNameKey{ "dark" }; static constexpr wchar_t RegKeyDwm[] = L"Software\\Microsoft\\Windows\\DWM"; static constexpr wchar_t RegKeyAccentColor[] = L"AccentColor"; @@ -320,3 +323,42 @@ winrt::WUX::ElementTheme Theme::RequestedTheme() const noexcept { return _Window ? _Window.RequestedTheme() : winrt::WUX::ElementTheme::Default; } + +winrt::com_ptr ThemePair::FromJson(const Json::Value& json) +{ + auto result = winrt::make_self(L"dark"); + + if (json.isString()) + { + result->_DarkName = result->_LightName = JsonUtils::GetValue(json); + } + else if (json.isObject()) + { + JsonUtils::GetValueForKey(json, DarkNameKey, result->_DarkName); + JsonUtils::GetValueForKey(json, LightNameKey, result->_LightName); + } + return result; +} + +Json::Value ThemePair::ToJson() const +{ + if (_DarkName == _LightName) + { + return JsonUtils::ConversionTrait().ToJson(DarkName()); + } + else + { + Json::Value json{ Json::ValueType::objectValue }; + + JsonUtils::SetValueForKey(json, DarkNameKey, _DarkName); + JsonUtils::SetValueForKey(json, LightNameKey, _LightName); + return json; + } +} +winrt::com_ptr ThemePair::Copy() const +{ + auto pair{ winrt::make_self() }; + pair->_DarkName = _DarkName; + pair->_LightName = _LightName; + return pair; +} diff --git a/src/cascadia/TerminalSettingsModel/Theme.h b/src/cascadia/TerminalSettingsModel/Theme.h index e39d42b8f20..4e0821df275 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.h +++ b/src/cascadia/TerminalSettingsModel/Theme.h @@ -22,10 +22,33 @@ Author(s): #include "WindowTheme.g.h" #include "TabRowTheme.g.h" #include "TabTheme.g.h" +#include "ThemePair.g.h" #include "Theme.g.h" +#include "JsonUtils.h" + namespace winrt::Microsoft::Terminal::Settings::Model::implementation { + struct ThemePair : ThemePairT + { + public: + ThemePair() = default; + explicit ThemePair(const winrt::hstring& name) noexcept : + _DarkName{ name }, + _LightName{ name } {}; + + explicit ThemePair(const winrt::hstring& lightName, const winrt::hstring& darkName) noexcept : + _DarkName{ darkName }, + _LightName{ lightName } {}; + + static com_ptr FromJson(const Json::Value& json); + Json::Value ToJson() const; + com_ptr Copy() const; + + WINRT_PROPERTY(winrt::hstring, DarkName); + WINRT_PROPERTY(winrt::hstring, LightName); + }; + struct ThemeColor : ThemeColorT { public: @@ -92,4 +115,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation { BASIC_FACTORY(ThemeColor); BASIC_FACTORY(Theme); + BASIC_FACTORY(ThemePair); +} + +namespace Microsoft::Terminal::Settings::Model::JsonUtils +{ + template<> + struct ConversionTrait + { + winrt::Microsoft::Terminal::Settings::Model::ThemePair FromJson(const Json::Value& json) + { + return *winrt::Microsoft::Terminal::Settings::Model::implementation::ThemePair::FromJson(json); + } + + bool CanConvert(const Json::Value& json) const + { + return json.isObject() || json.isString(); + } + + Json::Value ToJson(const winrt::Microsoft::Terminal::Settings::Model::ThemePair& val) + { + return winrt::get_self(val)->ToJson(); + } + + std::string TypeDescription() const + { + return "ThemePair{ string, string }"; + } + }; } diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 7078030fa9b..2eaa1f2f18f 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -18,6 +18,16 @@ namespace Microsoft.Terminal.Settings.Model Never }; + [default_interface] runtimeclass ThemePair + { + ThemePair(); + ThemePair(String name); + ThemePair(String darkName, String lightName); + + String DarkName; + String LightName; + }; + runtimeclass ThemeColor { ThemeColor();