diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index d44c2ddf837..0b8c0d91a9f 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -30,6 +30,8 @@ DERR dlldata DONTADDTORECENT DWORDLONG +DWMSBT +DWMWA endfor enumset environstrings @@ -95,6 +97,7 @@ lround Lsa lsass LSHIFT +MAINWINDOW memchr memicmp MENUCOMMAND @@ -176,6 +179,8 @@ Stubless Subheader Subpage syscall +SYSTEMBACKDROP +TABROW TASKBARCREATED TBPF THEMECHANGED diff --git a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj index 47214270725..76e0f07e293 100644 --- a/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj +++ b/src/cascadia/LocalTests_SettingsModel/SettingsModel.LocalTests.vcxproj @@ -41,6 +41,7 @@ + Create diff --git a/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp new file mode 100644 index 00000000000..25e115a0cd7 --- /dev/null +++ b/src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "../TerminalSettingsModel/Theme.h" +#include "../TerminalSettingsModel/CascadiaSettings.h" +#include "../types/inc/colorTable.hpp" +#include "JsonTestClass.h" + +#include + +using namespace Microsoft::Console; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace SettingsModelLocalTests +{ + // TODO:microsoft/terminal#3838: + // Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for + // an updated TAEF that will let us install framework packages when the test + // package is deployed. Until then, these tests won't deploy in CI. + + class ThemeTests : public JsonTestClass + { + // Use a custom AppxManifest to ensure that we can activate winrt types + // from our test. This property will tell taef to manually use this as + // the AppxManifest for this test class. + // This does not yet work for anything XAML-y. See TabTests.cpp for more + // details on that. + BEGIN_TEST_CLASS(ThemeTests) + TEST_CLASS_PROPERTY(L"RunAs", L"UAP") + TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") + END_TEST_CLASS() + + TEST_METHOD(ParseSimpleTheme); + TEST_METHOD(ParseEmptyTheme); + TEST_METHOD(ParseNoWindowTheme); + TEST_METHOD(ParseNullWindowTheme); + TEST_METHOD(ParseThemeWithNullThemeColor); + TEST_METHOD(InvalidCurrentTheme); + + static Core::Color rgb(uint8_t r, uint8_t g, uint8_t b) noexcept + { + return Core::Color{ r, g, b, 255 }; + } + }; + + void ThemeTests::ParseSimpleTheme() + { + static constexpr std::string_view orangeTheme{ R"({ + "name": "orange", + "tabRow": + { + "background": "#FFFF8800", + "unfocusedBackground": "#FF884400" + }, + "window": + { + "applicationTheme": "light", + "useMica": true + } + })" }; + + const auto schemeObject = VerifyParseSucceeded(orangeTheme); + auto theme = Theme::FromJson(schemeObject); + VERIFY_ARE_EQUAL(L"orange", theme->Name()); + + VERIFY_IS_NOT_NULL(theme->TabRow()); + VERIFY_IS_NOT_NULL(theme->TabRow().Background()); + VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); + VERIFY_ARE_EQUAL(rgb(0xff, 0x88, 0x00), theme->TabRow().Background().Color()); + + VERIFY_IS_NOT_NULL(theme->Window()); + VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Light, theme->Window().RequestedTheme()); + VERIFY_ARE_EQUAL(true, theme->Window().UseMica()); + } + + void ThemeTests::ParseEmptyTheme() + { + Log::Comment(L"This theme doesn't have any elements defined."); + static constexpr std::string_view emptyTheme{ R"({ + "name": "empty" + })" }; + + const auto schemeObject = VerifyParseSucceeded(emptyTheme); + auto theme = Theme::FromJson(schemeObject); + VERIFY_ARE_EQUAL(L"empty", theme->Name()); + VERIFY_IS_NULL(theme->TabRow()); + VERIFY_IS_NULL(theme->Window()); + VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); + } + + void ThemeTests::ParseNoWindowTheme() + { + Log::Comment(L"This theme doesn't have a window defined."); + static constexpr std::string_view emptyTheme{ R"({ + "name": "noWindow", + "tabRow": + { + "background": "#FF112233", + "unfocusedBackground": "#FF884400" + }, + })" }; + + const auto schemeObject = VerifyParseSucceeded(emptyTheme); + auto theme = Theme::FromJson(schemeObject); + VERIFY_ARE_EQUAL(L"noWindow", theme->Name()); + + VERIFY_IS_NOT_NULL(theme->TabRow()); + VERIFY_IS_NOT_NULL(theme->TabRow().Background()); + VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); + VERIFY_ARE_EQUAL(rgb(0x11, 0x22, 0x33), theme->TabRow().Background().Color()); + + VERIFY_IS_NULL(theme->Window()); + VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); + } + + void ThemeTests::ParseNullWindowTheme() + { + Log::Comment(L"This theme doesn't have a window defined."); + static constexpr std::string_view emptyTheme{ R"({ + "name": "nullWindow", + "tabRow": + { + "background": "#FF112233", + "unfocusedBackground": "#FF884400" + }, + "window": null + })" }; + + const auto schemeObject = VerifyParseSucceeded(emptyTheme); + auto theme = Theme::FromJson(schemeObject); + VERIFY_ARE_EQUAL(L"nullWindow", theme->Name()); + + VERIFY_IS_NOT_NULL(theme->TabRow()); + VERIFY_IS_NOT_NULL(theme->TabRow().Background()); + VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); + VERIFY_ARE_EQUAL(rgb(0x11, 0x22, 0x33), theme->TabRow().Background().Color()); + + VERIFY_IS_NULL(theme->Window()); + VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); + } + + void ThemeTests::ParseThemeWithNullThemeColor() + { + Log::Comment(L"These themes are all missing a tabRow background. Make sure we don't somehow default-construct one for them"); + + static constexpr std::string_view settingsString{ R"json({ + "themes": [ + { + "name": "backgroundEmpty", + "tabRow": + { + }, + "window": + { + "applicationTheme": "light", + "useMica": true + } + }, + { + "name": "backgroundNull", + "tabRow": + { + "background": null + }, + "window": + { + "applicationTheme": "light", + "useMica": true + } + }, + { + "name": "backgroundOmittedEntirely", + "window": + { + "applicationTheme": "light", + "useMica": true + } + } + ] + })json" }; + + try + { + const auto settings{ winrt::make_self(settingsString, DefaultJson) }; + + const auto& themes{ settings->GlobalSettings().Themes() }; + { + const auto& backgroundEmpty{ themes.Lookup(L"backgroundEmpty") }; + VERIFY_ARE_EQUAL(L"backgroundEmpty", backgroundEmpty.Name()); + VERIFY_IS_NOT_NULL(backgroundEmpty.TabRow()); + VERIFY_IS_NULL(backgroundEmpty.TabRow().Background()); + } + { + const auto& backgroundNull{ themes.Lookup(L"backgroundNull") }; + VERIFY_ARE_EQUAL(L"backgroundNull", backgroundNull.Name()); + VERIFY_IS_NOT_NULL(backgroundNull.TabRow()); + VERIFY_IS_NULL(backgroundNull.TabRow().Background()); + } + { + const auto& backgroundOmittedEntirely{ themes.Lookup(L"backgroundOmittedEntirely") }; + VERIFY_ARE_EQUAL(L"backgroundOmittedEntirely", backgroundOmittedEntirely.Name()); + VERIFY_IS_NULL(backgroundOmittedEntirely.TabRow()); + } + } + catch (const SettingsException& ex) + { + auto loadError = ex.Error(); + loadError; + throw ex; + } + catch (const SettingsTypedDeserializationException& e) + { + auto deserializationErrorMessage = til::u8u16(e.what()); + Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); + throw e; + } + } + + void ThemeTests::InvalidCurrentTheme() + { + Log::Comment(L"Make sure specifying an invalid theme falls back to a sensible default."); + + static constexpr std::string_view settingsString{ R"json({ + "theme": "foo", + "themes": [ + { + "name": "bar", + "tabRow": {}, + "window": + { + "applicationTheme": "light", + "useMica": true + } + } + ] + })json" }; + + try + { + const auto settings{ winrt::make_self(settingsString, DefaultJson) }; + + VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); + VERIFY_ARE_EQUAL(Settings::Model::SettingsLoadWarnings::UnknownTheme, settings->Warnings().GetAt(0)); + + const auto& themes{ settings->GlobalSettings().Themes() }; + { + const auto& bar{ themes.Lookup(L"bar") }; + VERIFY_ARE_EQUAL(L"bar", bar.Name()); + VERIFY_IS_NOT_NULL(bar.TabRow()); + VERIFY_IS_NULL(bar.TabRow().Background()); + } + + const auto currentTheme{ settings->GlobalSettings().CurrentTheme() }; + VERIFY_IS_NOT_NULL(currentTheme); + VERIFY_ARE_EQUAL(L"system", currentTheme.Name()); + } + catch (const SettingsException& ex) + { + auto loadError = ex.Error(); + loadError; + throw ex; + } + catch (const SettingsTypedDeserializationException& e) + { + auto deserializationErrorMessage = til::u8u16(e.what()); + Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); + throw e; + } + } +} diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 8f58309ff53..4a05ef2f21d 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -51,6 +51,7 @@ static const std::array settingsLoadWarningsLabels { USES_RESOURCE(L"InvalidSplitSize"), USES_RESOURCE(L"FailedToParseStartupActions"), USES_RESOURCE(L"FailedToParseSubCommands"), + USES_RESOURCE(L"UnknownTheme"), }; static const std::array settingsLoadErrorsLabels { USES_RESOURCE(L"NoProfilesText"), @@ -367,11 +368,12 @@ namespace winrt::TerminalApp::implementation // details here, but it does have the desired effect. // It's not enough to set the theme on the dialog alone. auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) { - auto theme{ _settings.GlobalSettings().Theme() }; + auto theme{ _settings.GlobalSettings().CurrentTheme() }; + auto requestedTheme{ theme.RequestedTheme() }; auto element{ sender.try_as() }; while (element) { - element.RequestedTheme(theme); + element.RequestedTheme(requestedTheme); element = element.Parent().try_as(); } } }; @@ -737,13 +739,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::ElementTheme AppLogic::GetRequestedTheme() { - if (!_loadedInitialSettings) - { - // Load settings if we haven't already - LoadSettings(); - } - - return _settings.GlobalSettings().Theme(); + return Theme().RequestedTheme(); } bool AppLogic::GetShowTabsInTitlebar() @@ -964,7 +960,7 @@ namespace winrt::TerminalApp::implementation void AppLogic::_RefreshThemeRoutine() { - _ApplyTheme(_settings.GlobalSettings().Theme()); + _ApplyTheme(GetRequestedTheme()); } // Function Description: @@ -1219,6 +1215,19 @@ namespace winrt::TerminalApp::implementation return {}; } + winrt::Windows::UI::Xaml::Media::Brush AppLogic::TitlebarBrush() + { + if (_root) + { + return _root->TitlebarBrush(); + } + return { nullptr }; + } + void AppLogic::WindowActivated(const bool activated) + { + _root->WindowActivated(activated); + } + bool AppLogic::HasCommandlineArguments() const noexcept { return _hasCommandLineArguments; @@ -1645,4 +1654,15 @@ namespace winrt::TerminalApp::implementation { return _settings.GlobalSettings().ShowTitleInTitlebar(); } + + Microsoft::Terminal::Settings::Model::Theme AppLogic::Theme() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + return _settings.GlobalSettings().CurrentTheme(); + } + } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 63d1c4e784d..2ba2deaeffb 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -117,6 +117,8 @@ namespace winrt::TerminalApp::implementation void WindowVisibilityChanged(const bool showOrHide); winrt::TerminalApp::TaskbarState TaskbarState(); + winrt::Windows::UI::Xaml::Media::Brush TitlebarBrush(); + void WindowActivated(const bool activated); bool GetMinimizeToNotificationArea(); bool GetAlwaysShowNotificationIcon(); @@ -127,7 +129,13 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Collections::IMapView GlobalHotkeys(); + Microsoft::Terminal::Settings::Model::Theme Theme(); + // -------------------------------- WinRT Events --------------------------------- + // PropertyChanged is surprisingly not a typed event, so we'll define that one manually. + winrt::event_token PropertyChanged(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& handler) { return _root->PropertyChanged(handler); } + void PropertyChanged(winrt::event_token const& token) { _root->PropertyChanged(token); } + TYPED_EVENT(RequestedThemeChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::ElementTheme); TYPED_EVENT(SettingsChanged, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(SystemMenuChangeRequested, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::SystemMenuChangeArgs); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index a5c0eb05a7b..42096bd08d1 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -35,7 +35,7 @@ namespace TerminalApp // See IDialogPresenter and TerminalPage's DialogPresenter for more // information. - [default_interface] runtimeclass AppLogic : IDirectKeyListener, IDialogPresenter + [default_interface] runtimeclass AppLogic : IDirectKeyListener, IDialogPresenter, Windows.UI.Xaml.Data.INotifyPropertyChanged { AppLogic(); @@ -94,6 +94,8 @@ namespace TerminalApp void WindowVisibilityChanged(Boolean showOrHide); TaskbarState TaskbarState{ get; }; + Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; + void WindowActivated(Boolean activated); Boolean ShouldUsePersistedLayout(); Boolean ShouldImmediatelyHandoffToElevated(); @@ -105,6 +107,8 @@ namespace TerminalApp Boolean GetAlwaysShowNotificationIcon(); Boolean GetShowTitleInTitlebar(); + Microsoft.Terminal.Settings.Model.Theme Theme { get; }; + FindTargetWindowResult FindTargetWindow(String[] args); Windows.Foundation.Collections.IMapView GlobalHotkeys(); diff --git a/src/cascadia/TerminalApp/HighlightedText.h b/src/cascadia/TerminalApp/HighlightedText.h index e71c7ef7523..c9c29e7cbb8 100644 --- a/src/cascadia/TerminalApp/HighlightedText.h +++ b/src/cascadia/TerminalApp/HighlightedText.h @@ -3,8 +3,6 @@ #pragma once -#include "winrt/Microsoft.UI.Xaml.Controls.h" - #include "HighlightedTextSegment.g.h" #include "HighlightedText.g.h" diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index a0a653927e3..29e70316f88 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -242,6 +242,10 @@ • Found a keybinding that was missing a required parameter value. This keybinding will be ignored. {Locked="•"} This glyph is a bullet, used in a bulleted list. + + • The specified "theme" was not found in the list of themes. Temporarily falling back to the default value. + {Locked="•"} This glyph is a bullet, used in a bulleted list. + The "globals" property is deprecated - your settings might need updating. {Locked="\"globals\""} diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 47cfa70d7b2..d88b7a7b4fe 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -154,6 +154,8 @@ namespace winrt::TerminalApp::implementation // Possibly update the icon of the tab. page->_UpdateTabIcon(*tab); + page->_updateTabRowColors(); + // Update the taskbar progress as well. We'll raise our own // SetTaskbarProgress event here, to get tell the hosting // application to re-query this value from us. @@ -925,6 +927,8 @@ namespace winrt::TerminalApp::implementation _TitleChangedHandlers(*this, tab.Title()); } + _updateTabRowColors(); + auto tab_impl = _GetTerminalTabImpl(tab); if (tab_impl) { diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 197cbce47c6..a975a7f6149 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -183,43 +183,6 @@ namespace winrt::TerminalApp::implementation const auto isElevated = IsElevated(); - if (_settings.GlobalSettings().UseAcrylicInTabRow()) - { - const auto res = Application::Current().Resources(); - const auto lightKey = winrt::box_value(L"Light"); - const auto darkKey = winrt::box_value(L"Dark"); - const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); - - for (const auto& dictionary : res.MergedDictionaries()) - { - // Don't change MUX resources - if (dictionary.Source()) - { - continue; - } - - for (const auto& kvPair : dictionary.ThemeDictionaries()) - { - const auto themeDictionary = kvPair.Value().as(); - - if (themeDictionary.HasKey(tabViewBackgroundKey)) - { - const auto backgroundSolidBrush = themeDictionary.Lookup(tabViewBackgroundKey).as(); - - const til::color backgroundColor = backgroundSolidBrush.Color(); - - const auto acrylicBrush = Media::AcrylicBrush(); - acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); - acrylicBrush.FallbackColor(backgroundColor); - acrylicBrush.TintColor(backgroundColor); - acrylicBrush.TintOpacity(0.5); - - themeDictionary.Insert(tabViewBackgroundKey, acrylicBrush); - } - } - } - } - _tabRow.PointerMoved({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); _tabView.CanReorderTabs(!isElevated); _tabView.CanDragTabs(!isElevated); @@ -260,6 +223,7 @@ namespace winrt::TerminalApp::implementation transparent.Color(Windows::UI::Colors::Transparent()); _tabRow.Background(transparent); } + _updateTabRowColors(); // Hookup our event handlers to the ShortcutActionDispatch _RegisterActionCallbacks(); @@ -1471,6 +1435,16 @@ namespace winrt::TerminalApp::implementation term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler }); + term.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& e) { + if (auto page{ weakThis.get() }) + { + if (e.PropertyName() == L"BackgroundBrush") + { + page->_updateTabRowColors(); + } + } + }); + term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); } @@ -1510,37 +1484,9 @@ namespace winrt::TerminalApp::implementation } }); - // react on color changed events - hostingTab.ColorSelected([weakTab, weakThis](auto&& color) { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab && (tab->FocusState() != FocusState::Unfocused)) - { - page->_SetNonClientAreaColors(color); - } - }); - - hostingTab.ColorCleared([weakTab, weakThis]() { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - - if (page && tab && (tab->FocusState() != FocusState::Unfocused)) - { - page->_ClearNonClientAreaColors(); - } - }); - // Add an event handler for when the terminal or tab wants to set a // progress indicator on the taskbar hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - - // TODO GH#3327: Once we support colorizing the NewTab button based on - // the color of the tab, we'll want to make sure to call - // _ClearNewTabButtonColor here, to reset it to the default (for the - // newly created tab). - // remove any colors left by other colored tabs - // _ClearNewTabButtonColor(); } // Method Description: @@ -2750,6 +2696,13 @@ namespace winrt::TerminalApp::implementation WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); _tabRow.ShowElevationShield(IsElevated() && _settings.GlobalSettings().ShowAdminShield()); + + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; + _tabView.Background(transparent); + + //////////////////////////////////////////////////////////////////////// + // Begin Theme handling + _updateTabRowColors(); } // This is a helper to aid in sorting commands by their `Name`s, alphabetically. @@ -3134,32 +3087,6 @@ namespace winrt::TerminalApp::implementation _newTabButton.Foreground(foregroundBrush); } - // Method Description: - // - Sets the tab split button color when a new tab color is selected - // - This method could also set the color of the title bar and tab row - // in the future - // Arguments: - // - selectedTabColor: The color of the newly selected tab - // Return Value: - // - - void TerminalPage::_SetNonClientAreaColors(const Windows::UI::Color& /*selectedTabColor*/) - { - // TODO GH#3327: Look at what to do with the NC area when we have XAML theming - } - - // Method Description: - // - Clears the tab split button color when the tab's color is cleared - // - This method could also clear the color of the title bar and tab row - // in the future - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_ClearNonClientAreaColors() - { - // TODO GH#3327: Look at what to do with the NC area when we have XAML theming - } - // Function Description: // - This is a helper method to get the commandline out of a // ExecuteCommandline action, break it into subcommands, and attempt to @@ -3569,10 +3496,11 @@ namespace winrt::TerminalApp::implementation // - void TerminalPage::_UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element) { - auto theme{ _settings.GlobalSettings().Theme() }; + auto theme{ _settings.GlobalSettings().CurrentTheme() }; + auto requestedTheme{ theme.RequestedTheme() }; while (element) { - element.RequestedTheme(theme); + element.RequestedTheme(requestedTheme); element = element.Parent().try_as(); } } @@ -4076,4 +4004,132 @@ namespace winrt::TerminalApp::implementation applicationState.DismissedMessages(std::move(messages)); } + void TerminalPage::_updateTabRowColors() + { + if (_settings == nullptr) + { + return; + } + + const auto theme = _settings.GlobalSettings().CurrentTheme(); + const auto requestedTheme{ theme.RequestedTheme() }; + + const auto res = Application::Current().Resources(); + + // XAML Hacks: + // + // the App is always in the OS theme, so the + // App::Current().Resources() lookup will always get the value for the + // OS theme, not the requested theme. + // + // This helper allows us to instead lookup the value of a resource + // specified by `key` for the given `requestedTheme`, from the + // dictionaries in App.xaml. Make sure the value is actually there! + // Otherwise this'll throw like any other Lookup for a resource that + // isn't there. + static const auto lookup = [](auto& res, auto& requestedTheme, auto& key) { + // You want the Default version of the resource? Great, the App is + // always in the OS theme. Just look it up and be done. + if (requestedTheme == ElementTheme::Default) + { + return res.Lookup(key); + } + static const auto lightKey = winrt::box_value(L"Light"); + static const auto darkKey = winrt::box_value(L"Dark"); + // There isn't an ElementTheme::HighContrast. + + auto requestedThemeKey = requestedTheme == ElementTheme::Dark ? darkKey : lightKey; + for (const auto& dictionary : res.MergedDictionaries()) + { + // Don't look in the MUX resources. They come first. A person + // with more patience than me may find a way to look through our + // dictionaries first, then the MUX ones, but that's not needed + // currently + if (dictionary.Source()) + { + continue; + } + // Look through the theme dictionaries we defined: + for (const auto& [dictionaryKey, dict] : dictionary.ThemeDictionaries()) + { + // Does the key for this dict match the theme we're looking for? + if (winrt::unbox_value(dictionaryKey) != + winrt::unbox_value(requestedThemeKey)) + { + // No? skip it. + continue; + } + // Look for the requested resource in this dict. + const auto themeDictionary = dict.as(); + if (themeDictionary.HasKey(key)) + { + return themeDictionary.Lookup(key); + } + } + } + + // We didn't find it in the requested dict, fall back to the default dictionary. + return res.Lookup(key); + }; + + // Use our helper to lookup the theme-aware version of the resource. + const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); + const auto backgroundSolidBrush = lookup(res, requestedTheme, tabViewBackgroundKey).as(); + + til::color bgColor = backgroundSolidBrush.Color(); + + if (_settings.GlobalSettings().UseAcrylicInTabRow()) + { + const til::color backgroundColor = backgroundSolidBrush.Color(); + const auto acrylicBrush = Media::AcrylicBrush(); + acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + acrylicBrush.FallbackColor(bgColor); + acrylicBrush.TintColor(bgColor); + acrylicBrush.TintOpacity(0.5); + + TitlebarBrush(acrylicBrush); + } + else if (theme.TabRow() && theme.TabRow().Background()) + { + const auto tabRowBg = theme.TabRow().Background(); + const auto terminalBrush = [this]() -> Media::Brush { + if (const auto& control{ _GetActiveControl() }) + { + return control.BackgroundBrush(); + } + else if (auto settingsTab = _GetFocusedTab().try_as()) + { + return settingsTab.Content().try_as().BackgroundBrush(); + } + return nullptr; + }(); + + const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) }; + bgColor = ThemeColor::ColorFromBrush(themeBrush); + TitlebarBrush(themeBrush); + } + else + { + // Nothing was set in the theme - fall back to our original `TabViewBackground` color. + TitlebarBrush(backgroundSolidBrush); + } + + if (!_settings.GlobalSettings().ShowTabsInTitlebar()) + { + _tabRow.Background(TitlebarBrush()); + } + + // Update the new tab button to have better contrast with the new color. + // In theory, it would be convenient to also change these for the + // inactive tabs as well, but we're leaving that as a follow up. + _SetNewTabButtonColor(bgColor, bgColor); + } + + void TerminalPage::WindowActivated(const bool activated) + { + // Stash if we're activated. Use that when we reload + // the settings, change active panes, etc. + _activated = activated; + _updateTabRowColors(); + } } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 310910f5228..f4e8091dd93 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -134,6 +134,7 @@ namespace winrt::TerminalApp::implementation bool IsElevated() const noexcept; void OpenSettingsUI(); + void WindowActivated(const bool activated); WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); @@ -157,6 +158,8 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(QuitRequested, IInspectable, IInspectable); TYPED_EVENT(ShowWindowChanged, IInspectable, winrt::Microsoft::Terminal::Control::ShowWindowArgs) + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, _PropertyChangedHandlers, nullptr); + private: friend struct TerminalPageT; // for Xaml to bind events std::optional _hostingHwnd; @@ -196,6 +199,8 @@ namespace winrt::TerminalApp::implementation std::optional _rearrangeFrom{}; std::optional _rearrangeTo{}; bool _removing{ false }; + + bool _activated{ false }; bool _visible{ true }; std::vector> _previouslyClosedPanesAndTabs{}; @@ -383,8 +388,6 @@ namespace winrt::TerminalApp::implementation void _RefreshUIForSettingsReload(); - void _SetNonClientAreaColors(const Windows::UI::Color& selectedTabColor); - void _ClearNonClientAreaColors(); void _SetNewTabButtonColor(const Windows::UI::Color& color, const Windows::UI::Color& accentColor); void _ClearNewTabButtonColor(); @@ -443,6 +446,8 @@ namespace winrt::TerminalApp::implementation static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); + void _updateTabRowColors(); + winrt::fire_and_forget _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); #pragma region ActionHandlers diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl index 90d317c1d98..89ea3ec3277 100644 --- a/src/cascadia/TerminalApp/TerminalPage.idl +++ b/src/cascadia/TerminalApp/TerminalPage.idl @@ -45,6 +45,9 @@ namespace TerminalApp TaskbarState TaskbarState{ get; }; + Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; + void WindowActivated(Boolean activated); + event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler LastTabClosed; event Windows.Foundation.TypedEventHandler SetTitleBarContent; diff --git a/src/cascadia/TerminalApp/TitlebarControl.xaml b/src/cascadia/TerminalApp/TitlebarControl.xaml index fde78d3a81a..e5eda4f5041 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.xaml +++ b/src/cascadia/TerminalApp/TitlebarControl.xaml @@ -13,7 +13,6 @@ VerticalAlignment="Top" d:DesignHeight="36" d:DesignWidth="400" - Background="{ThemeResource TabViewBackground}" SizeChanged="Root_SizeChanged" mc:Ignorable="d"> diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 7a1b57df3a6..3122093d19c 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -568,6 +568,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation RootGrid().Background(solidColor); } + + BackgroundBrush(RootGrid().Background()); } // Method Description: @@ -613,6 +615,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation { solidColor.Color(bg); } + + BackgroundBrush(RootGrid().Background()); + + // Don't use the normal BackgroundBrush() Observable Property setter + // here. (e.g. `BackgroundBrush()`). The one from the macro will + // automatically ignore changes where the value doesn't _actually_ + // change. In our case, most of the time when changing the colors of the + // background, the _Brush_ itself doesn't change, we simply change the + // Color() of the brush. This results in the event not getting bubbled + // up. + // + // Firing it manually makes sure it does. + _BackgroundBrush = RootGrid().Background(); + _PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"BackgroundBrush" }); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index e67f7c773ce..655fce03fdb 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -130,6 +130,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void AdjustOpacity(const double opacity, const bool relative); + WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler); + // -------------------------------- WinRT Events --------------------------------- // clang-format off WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); @@ -153,6 +155,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(WarningBell, IInspectable, IInspectable); // clang-format on + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, BackgroundBrush, _PropertyChangedHandlers, nullptr); + private: friend struct TermControlT; // friend our parent so it can bind private event handlers diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 3d8cde2714e..101359b30e9 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -14,7 +14,8 @@ namespace Microsoft.Terminal.Control [default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IDirectKeyListener, IMouseWheelListener, - ICoreState + ICoreState, + Windows.UI.Xaml.Data.INotifyPropertyChanged { TermControl(IControlSettings settings, IControlAppearance unfocusedAppearance, @@ -90,5 +91,6 @@ namespace Microsoft.Terminal.Control // opacity set by the settings should call this instead. Double BackgroundOpacity { get; }; + Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; } } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp index b34bf0a1e95..1c6032b8329 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.cpp @@ -41,17 +41,22 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation L"zh-Hant", }; - GlobalAppearance::GlobalAppearance() + constexpr std::wstring_view systemThemeName{ L"system" }; + constexpr std::wstring_view darkThemeName{ L"dark" }; + constexpr std::wstring_view lightThemeName{ L"light" }; + + GlobalAppearance::GlobalAppearance() : + _ThemeList{ single_threaded_observable_vector() } { InitializeComponent(); - INITIALIZE_BINDABLE_ENUM_SETTING(Theme, ElementTheme, winrt::Windows::UI::Xaml::ElementTheme, L"Globals_Theme", L"Content"); INITIALIZE_BINDABLE_ENUM_SETTING(TabWidthMode, TabViewWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, L"Globals_TabWidthMode", L"Content"); } void GlobalAppearance::OnNavigatedTo(const NavigationEventArgs& e) { _State = e.Parameter().as(); + _UpdateThemeList(); } winrt::hstring GlobalAppearance::LanguageDisplayConverter(const winrt::hstring& tag) @@ -195,4 +200,61 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } + // Function Description: + // - Updates the list of all themes available to choose from. + void GlobalAppearance::_UpdateThemeList() + { + // Surprisingly, though this is called every time we navigate to the page, + // the list does not keep growing on each navigation. + const auto& ThemeMap{ _State.Globals().Themes() }; + for (const auto& pair : ThemeMap) + { + _ThemeList.Append(pair.Value()); + } + } + + winrt::Windows::Foundation::IInspectable GlobalAppearance::CurrentTheme() + { + return _State.Globals().CurrentTheme(); + } + + // Get the name out of the newly selected item, stash that as the Theme name + // set for the globals. That controls which theme is actually the current + // theme. + void GlobalAppearance::CurrentTheme(const winrt::Windows::Foundation::IInspectable& tag) + { + if (const auto& theme{ tag.try_as() }) + { + _State.Globals().Theme(theme.Name()); + } + } + + // Method Description: + // - Convert the names of the inbox themes to some more descriptive, + // well-known values. If the passed in theme isn't an inbox one, then just + // return its set Name. + // - "light" becomes "Light" + // - "dark" becomes "Dark" + // - "system" becomes "Use Windows theme" + // - These values are all localized based on the app language. + // Arguments: + // - theme: the theme to get the display name for. + // Return Value: + // - the potentially localized name to use for this Theme. + winrt::hstring GlobalAppearance::ThemeNameConverter(const Model::Theme& theme) + { + if (theme.Name() == darkThemeName) + { + return RS_(L"Globals_ThemeDark/Content"); + } + else if (theme.Name() == lightThemeName) + { + return RS_(L"Globals_ThemeLight/Content"); + } + else if (theme.Name() == systemThemeName) + { + return RS_(L"Globals_ThemeSystem/Content"); + } + return theme.Name(); + } } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h index 7e48178baf4..3da730eb6c6 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.h @@ -26,9 +26,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void OnNavigatedTo(const winrt::Windows::UI::Xaml::Navigation::NavigationEventArgs& e); WINRT_PROPERTY(Editor::GlobalAppearancePageNavigationState, State, nullptr); - GETSET_BINDABLE_ENUM_SETTING(Theme, winrt::Windows::UI::Xaml::ElementTheme, State().Globals().Theme); GETSET_BINDABLE_ENUM_SETTING(TabWidthMode, winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, State().Globals().TabWidthMode); + WINRT_PROPERTY(Windows::Foundation::Collections::IObservableVector, ThemeList, nullptr); + public: // LanguageDisplayConverter maps the given BCP 47 tag to a localized string. // For instance "en-US" produces "English (United States)", while "de-DE" produces @@ -40,9 +41,16 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation winrt::Windows::Foundation::IInspectable CurrentLanguage(); void CurrentLanguage(const winrt::Windows::Foundation::IInspectable& tag); + winrt::Windows::Foundation::IInspectable CurrentTheme(); + void CurrentTheme(const winrt::Windows::Foundation::IInspectable& tag); + static winrt::hstring ThemeNameConverter(const Model::Theme& theme); + private: winrt::Windows::Foundation::Collections::IObservableVector _languageList; winrt::Windows::Foundation::IInspectable _currentLanguage; + winrt::Windows::Foundation::IInspectable _currentTheme; + + void _UpdateThemeList(); }; } diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl index 7e3483140ab..a45ba503fc6 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.idl @@ -21,7 +21,8 @@ namespace Microsoft.Terminal.Settings.Editor IInspectable CurrentLanguage; IInspectable CurrentTheme; - Windows.Foundation.Collections.IObservableVector ThemeList { get; }; + static String ThemeNameConverter(Microsoft.Terminal.Settings.Model.Theme theme); + Windows.Foundation.Collections.IObservableVector ThemeList { get; }; IInspectable CurrentTabWidthMode; Windows.Foundation.Collections.IObservableVector TabWidthModeList { get; }; diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index 2d15f1c2103..980b5323acb 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -8,6 +8,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Microsoft.Terminal.Settings.Editor" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:model="using:Microsoft.Terminal.Settings.Model" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" mc:Ignorable="d"> @@ -42,10 +43,15 @@ + Style="{StaticResource ComboBoxSettingStyle}"> + + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index 2009502c569..45ac57129ef 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -609,4 +609,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { return _breadcrumbs; } + + winrt::Windows::UI::Xaml::Media::Brush MainPage::BackgroundBrush() + { + return SettingsNav().Background(); + } + } diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index 2f7d5388cdb..ac257da0beb 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -40,6 +40,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation bool TryPropagateHostingWindow(IInspectable object) noexcept; uint64_t GetHostingWindow() const noexcept; + winrt::Windows::UI::Xaml::Media::Brush BackgroundBrush(); + Windows::Foundation::Collections::IObservableVector Breadcrumbs() noexcept; TYPED_EVENT(OpenJson, Windows::Foundation::IInspectable, Model::SettingsTarget); diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.idl b/src/cascadia/TerminalSettingsEditor/MainPage.idl index 4c771419753..2b724ab67b4 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.idl +++ b/src/cascadia/TerminalSettingsEditor/MainPage.idl @@ -39,5 +39,7 @@ namespace Microsoft.Terminal.Settings.Editor void SetHostingWindow(UInt64 window); Windows.Foundation.Collections.IObservableVector Breadcrumbs { get; }; + + Windows.UI.Xaml.Media.Brush BackgroundBrush { get; }; } } diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp index 8a5b98ea5c9..4154786c303 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp @@ -409,6 +409,7 @@ void CascadiaSettings::_validateSettings() _validateMediaResources(); _validateKeybindings(); _validateColorSchemesInCommands(); + _validateThemeExists(); } // Method Description: @@ -1152,3 +1153,14 @@ void CascadiaSettings::ExportFile(winrt::hstring path, winrt::hstring content) } CATCH_LOG(); } + +void CascadiaSettings::_validateThemeExists() +{ + if (!_globals->Themes().HasKey(_globals->Theme())) + { + _warnings.Append(SettingsLoadWarnings::UnknownTheme); + + // safely fall back to system as the theme. + _globals->Theme(L"system"); + } +} diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 737fa8075a8..c6141df700c 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -73,6 +73,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation const Json::Value& colorSchemes; const Json::Value& profileDefaults; const Json::Value& profilesList; + const Json::Value& themes; }; static std::pair _lineAndColumnFromPosition(const std::string_view& string, const size_t position); @@ -158,6 +159,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void _validateKeybindings() const; void _validateColorSchemesInCommands() const; bool _hasInvalidColorScheme(const Model::Command& command) const; + void _validateThemeExists(); // user settings winrt::com_ptr _globals = winrt::make_self(); diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index c8baba43e8c..f8e4dc4f80f 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -39,6 +39,7 @@ static constexpr std::string_view ProfilesKey{ "profiles" }; static constexpr std::string_view DefaultSettingsKey{ "defaults" }; static constexpr std::string_view ProfilesListKey{ "list" }; static constexpr std::string_view SchemesKey{ "schemes" }; +static constexpr std::string_view ThemesKey{ "themes" }; static constexpr std::wstring_view jsonExtension{ L".json" }; static constexpr std::wstring_view FragmentsSubDirectory{ L"\\Fragments" }; @@ -531,6 +532,25 @@ void SettingsLoader::_parse(const OriginTag origin, const winrt::hstring& source } } + { + for (const auto& themeJson : json.themes) + { + if (const auto theme = Theme::FromJson(themeJson)) + { + if (origin != OriginTag::InBox && + (theme->Name() == L"system" || theme->Name() == L"light" || theme->Name() == L"dark")) + { + // If the theme didn't come from the in-box themes, and its + // name was one of the reserved names, then just ignore it. + // Themes don't support layering - we don't want the user + // versions of these themes overriding the built-in ones. + continue; + } + settings.globals->AddTheme(*theme); + } + } + } + { settings.baseLayerProfile = Profile::FromJson(json.profileDefaults); // Remove the `guid` member from the default settings. @@ -629,10 +649,11 @@ SettingsLoader::JsonSettings SettingsLoader::_parseJson(const std::string_view& { auto root = content.empty() ? Json::Value{ Json::ValueType::objectValue } : _parseJSON(content); const auto& colorSchemes = _getJSONValue(root, SchemesKey); + const auto& themes = _getJSONValue(root, ThemesKey); const auto& profilesObject = _getJSONValue(root, ProfilesKey); const auto& profileDefaults = _getJSONValue(profilesObject, DefaultSettingsKey); const auto& profilesList = profilesObject.isArray() ? profilesObject : _getJSONValue(profilesObject, ProfilesListKey); - return JsonSettings{ std::move(root), colorSchemes, profileDefaults, profilesList }; + return JsonSettings{ std::move(root), colorSchemes, profileDefaults, profilesList, themes }; } // Just a common helper function between _parse and _parseFragment. @@ -1092,6 +1113,20 @@ Json::Value CascadiaSettings::ToJson() const } json[JsonKey(SchemesKey)] = schemes; + Json::Value themes{ Json::ValueType::arrayValue }; + for (const auto& entry : _globals->Themes()) + { + // Ignore the built in themes, when serializing the themes back out. We + // don't want to re-include them in the user settings file. + const auto theme{ winrt::get_self(entry.Value()) }; + if (theme->Name() == L"system" || theme->Name() == L"light" || theme->Name() == L"dark") + { + continue; + } + themes.append(theme->ToJson()); + } + json[JsonKey(ThemesKey)] = themes; + return json; } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index e417042e44e..64ddea29f06 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -17,6 +17,7 @@ using namespace winrt::Microsoft::UI::Xaml::Controls; static constexpr std::string_view LegacyKeybindingsKey{ "keybindings" }; static constexpr std::string_view ActionsKey{ "actions" }; +static constexpr std::string_view ThemeKey{ "theme" }; static constexpr std::string_view DefaultProfileKey{ "defaultProfile" }; static constexpr std::string_view LegacyUseTabSwitcherModeKey{ "useTabSwitcher" }; @@ -39,6 +40,14 @@ void GlobalAppSettings::_FinalizeInheritance() _colorSchemes.Insert(k, v); } } + + for (const auto& [k, v] : parent->_themes) + { + if (!_themes.HasKey(k)) + { + _themes.Insert(k, v); + } + } } } @@ -65,6 +74,14 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_colorSchemes.Insert(kv.Key(), *schemeImpl->Copy()); } } + if (_themes) + { + for (auto kv : _themes) + { + const auto themeImpl{ winrt::get_self(kv.Value()) }; + globals->_themes.Insert(kv.Key(), *themeImpl->Copy()); + } + } for (const auto& parent : _parents) { @@ -192,3 +209,18 @@ Json::Value GlobalAppSettings::ToJson() const json[JsonKey(ActionsKey)] = _actionMap->ToJson(); return json; } + +winrt::Microsoft::Terminal::Settings::Model::Theme GlobalAppSettings::CurrentTheme() noexcept +{ + return _themes.TryLookup(Theme()); +} + +void GlobalAppSettings::AddTheme(const Model::Theme& theme) +{ + _themes.Insert(theme.Name(), theme); +} + +winrt::Windows::Foundation::Collections::IMapView GlobalAppSettings::Themes() noexcept +{ + return _themes.GetView(); +} diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 14584b26a72..3c876c75d3f 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -22,6 +22,7 @@ Author(s): #include "ActionMap.h" #include "Command.h" #include "ColorScheme.h" +#include "Theme.h" // fwdecl unittest classes namespace SettingsModelLocalTests @@ -62,6 +63,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation DisableAnimations(!invertedDisableAnimationsValue); } + Windows::Foundation::Collections::IMapView Themes() noexcept; + void AddTheme(const Model::Theme& theme); + Model::Theme CurrentTheme() noexcept; + INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L""); #define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \ @@ -78,7 +83,9 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation winrt::guid _defaultProfile; winrt::com_ptr _actionMap{ winrt::make_self() }; + std::vector _keybindingsWarnings; Windows::Foundation::Collections::IMap _colorSchemes{ winrt::single_threaded_map() }; + Windows::Foundation::Collections::IMap _themes{ winrt::single_threaded_map() }; }; } diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 51386e6be34..c0226eaab6b 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -3,6 +3,7 @@ #include "IInheritable.idl.h" +import "Theme.idl"; import "ColorScheme.idl"; import "ActionMap.idl"; @@ -54,7 +55,6 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); INHERITABLE_SETTING(String, Language); - INHERITABLE_SETTING(Windows.UI.Xaml.ElementTheme, Theme); INHERITABLE_SETTING(Microsoft.UI.Xaml.Controls.TabViewWidthMode, TabWidthMode); INHERITABLE_SETTING(Boolean, UseAcrylicInTabRow); INHERITABLE_SETTING(Boolean, ShowTabsInTitlebar); @@ -94,5 +94,10 @@ namespace Microsoft.Terminal.Settings.Model void RemoveColorScheme(String schemeName); ActionMap ActionMap { get; }; + + Windows.Foundation.Collections.IMapView Themes(); + void AddTheme(Theme theme); + INHERITABLE_SETTING(String, Theme); + Theme CurrentTheme { get; }; } } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index e425bdfd877..259c22951b4 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -33,8 +33,8 @@ Author(s): X(bool, AlwaysShowTabs, "alwaysShowTabs", true) \ X(bool, ShowTitleInTitlebar, "showTerminalTitleInTitlebar", true) \ X(bool, ConfirmCloseAllTabs, "confirmCloseAllTabs", true) \ + X(hstring, Theme, "theme") \ X(hstring, Language, "language") \ - X(winrt::Windows::UI::Xaml::ElementTheme, Theme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default) \ X(winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode, TabWidthMode, "tabWidthMode", winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode::Equal) \ X(bool, UseAcrylicInTabRow, "useAcrylicInTabRow", false) \ X(bool, ShowTabsInTitlebar, "showTabsInTitlebar", true) \ @@ -114,3 +114,14 @@ Author(s): // Intentionally omitted Appearance settings: // * ForegroundKey, BackgroundKey, SelectionBackgroundKey, CursorColorKey: all optional colors // * Opacity: needs special parsing + +#define MTSM_THEME_SETTINGS(X) \ + X(winrt::Microsoft::Terminal::Settings::Model::WindowTheme, Window, "window", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::TabRowTheme, TabRow, "tabRow", nullptr) + +#define MTSM_THEME_WINDOW_SETTINGS(X) \ + X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "applicationTheme", winrt::Windows::UI::Xaml::ElementTheme::Default) \ + X(bool, UseMica, "useMica", false) + +#define MTSM_THEME_TABROW_SETTINGS(X) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index 21184811d31..678446dff77 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -48,6 +48,9 @@ ColorScheme.idl + + Theme.idl + Command.idl @@ -128,6 +131,9 @@ ColorScheme.idl + + Theme.idl + Command.idl @@ -173,6 +179,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h index 9dd273ee3c9..75e3fb63c1a 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h +++ b/src/cascadia/TerminalSettingsModel/TerminalSettingsSerializationHelpers.h @@ -547,6 +547,79 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage) }; }; +template<> +struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait +{ + winrt::Microsoft::Terminal::Settings::Model::ThemeColor FromJson(const Json::Value& json) + { + if (json == Json::Value::null) + { + return nullptr; + } + const auto string{ Detail::GetStringView(json) }; + if (string == "accent") + { + return winrt::Microsoft::Terminal::Settings::Model::ThemeColor::FromAccent(); + } + else if (string == "terminalBackground") + { + return winrt::Microsoft::Terminal::Settings::Model::ThemeColor::FromTerminalBackground(); + } + else + { + return winrt::Microsoft::Terminal::Settings::Model::ThemeColor::FromColor(::Microsoft::Console::Utils::ColorFromHexString(string)); + } + } + + bool CanConvert(const Json::Value& json) + { + if (json == Json::Value::null) + { + return true; + } + if (!json.isString()) + { + return false; + } + + const auto string{ Detail::GetStringView(json) }; + const auto isColorSpec = (string.length() == 9 || string.length() == 7 || string.length() == 4) && string.front() == '#'; + const auto isAccent = string == "accent"; + const auto isTerminalBackground = string == "terminalBackground"; + return isColorSpec || isAccent || isTerminalBackground; + } + + Json::Value ToJson(const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& val) + { + if (val == nullptr) + { + return Json::Value::null; + } + + switch (val.ColorType()) + { + case winrt::Microsoft::Terminal::Settings::Model::ThemeColorType::Accent: + { + return "accent"; + } + case winrt::Microsoft::Terminal::Settings::Model::ThemeColorType::Color: + { + return til::u16u8(til::color{ val.Color() }.ToHexString(false)); + } + case winrt::Microsoft::Terminal::Settings::Model::ThemeColorType::TerminalBackground: + { + return "terminalBackground"; + } + } + return til::u16u8(til::color{ val.Color() }.ToHexString(false)); + } + + std::string TypeDescription() const + { + return "ThemeColor (#rrggbb, #rgb, #aarrggbb, accent, terminalBackground)"; + } +}; + // Possible ScrollToMarkDirection values JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection) { diff --git a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl index c8aea6b95ab..43dee4e5a61 100644 --- a/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl +++ b/src/cascadia/TerminalSettingsModel/TerminalWarnings.idl @@ -21,6 +21,7 @@ namespace Microsoft.Terminal.Settings.Model InvalidSplitSize, FailedToParseStartupActions, FailedToParseSubCommands, + UnknownTheme, WARNINGS_SIZE // IMPORTANT: This MUST be the last value in this enum. It's an unused placeholder. }; diff --git a/src/cascadia/TerminalSettingsModel/Theme.cpp b/src/cascadia/TerminalSettingsModel/Theme.cpp new file mode 100644 index 00000000000..e4b770a6ced --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/Theme.cpp @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Theme.h" +#include "../../types/inc/Utils.hpp" +#include "../../types/inc/colorTable.hpp" +#include "Utils.h" +#include "JsonUtils.h" +#include "TerminalSettingsSerializationHelpers.h" + +#include "ThemeColor.g.cpp" +#include "WindowTheme.g.cpp" +#include "TabRowTheme.g.cpp" +#include "Theme.g.cpp" + +using namespace ::Microsoft::Console; +using namespace Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; +using namespace winrt::Windows::UI; + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; +} + +static constexpr std::string_view NameKey{ "name" }; + +static constexpr wchar_t RegKeyDwm[] = L"Software\\Microsoft\\Windows\\DWM"; +static constexpr wchar_t RegKeyAccentColor[] = L"AccentColor"; + +winrt::Microsoft::Terminal::Settings::Model::ThemeColor ThemeColor::FromColor(const winrt::Microsoft::Terminal::Core::Color& coreColor) noexcept +{ + auto result = winrt::make_self(); + result->_Color = coreColor; + result->_ColorType = ThemeColorType::Color; + return *result; +} + +winrt::Microsoft::Terminal::Settings::Model::ThemeColor ThemeColor::FromAccent() noexcept +{ + auto result = winrt::make_self(); + result->_ColorType = ThemeColorType::Accent; + return *result; +} + +winrt::Microsoft::Terminal::Settings::Model::ThemeColor ThemeColor::FromTerminalBackground() noexcept +{ + auto result = winrt::make_self(); + result->_ColorType = ThemeColorType::TerminalBackground; + return *result; +} + +static wil::unique_hkey openDwmRegKey() +{ + HKEY hKey{ nullptr }; + if (RegOpenKeyEx(HKEY_CURRENT_USER, RegKeyDwm, 0, KEY_READ, &hKey) == ERROR_SUCCESS) + { + return wil::unique_hkey{ hKey }; + } + return nullptr; +} +static DWORD readDwmSubValue(const wil::unique_hkey& dwmRootKey, const wchar_t* key) +{ + DWORD val{ 0 }; + DWORD size{ sizeof(val) }; + LOG_IF_FAILED(RegQueryValueExW(dwmRootKey.get(), key, nullptr, nullptr, reinterpret_cast(&val), &size)); + return val; +} + +static til::color _getAccentColorForTitlebar() +{ + // The color used for the "Use Accent color in the title bar" in DWM is + // stored in HKCU\Software\Microsoft\Windows\DWM\AccentColor. + return til::color{ static_cast(readDwmSubValue(openDwmRegKey(), RegKeyAccentColor)) }.with_alpha(255); +} + +til::color ThemeColor::ColorFromBrush(const winrt::WUX::Media::Brush& brush) +{ + if (auto acrylic = brush.try_as()) + { + return acrylic.TintColor(); + } + else if (auto solidColor = brush.try_as()) + { + return solidColor.Color(); + } + return {}; +} + +winrt::WUX::Media::Brush ThemeColor::Evaluate(const winrt::WUX::ResourceDictionary& res, + const winrt::WUX::Media::Brush& terminalBackground, + const bool forTitlebar) +{ + static const auto accentColorKey{ winrt::box_value(L"SystemAccentColor") }; + + // NOTE: Currently, the DWM titlebar is always drawn, underneath our XAML + // content. If the opacity is <1.0, then you'll be able to see it, including + // the original caption buttons, which we don't want. + + switch (ColorType()) + { + case ThemeColorType::Accent: + { + til::color accentColor = forTitlebar ? + _getAccentColorForTitlebar() : + til::color{ winrt::unbox_value(res.Lookup(accentColorKey)) }; + + const winrt::WUX::Media::SolidColorBrush accentBrush{ accentColor }; + // _getAccentColorForTitlebar should have already filled the alpha + // channel in with 255 + return accentBrush; + } + case ThemeColorType::Color: + { + return winrt::WUX::Media::SolidColorBrush{ forTitlebar ? + Color().with_alpha(255) : + Color() }; + } + case ThemeColorType::TerminalBackground: + { + // If we're evaluating this color for the tab row, there are some rules + // we have to follow, unfortunately. We can't allow a transparent + // background, so we have to make sure to fill that in with Opacity(1.0) + // manually. + // + // So for that case, just make a new brush with the relevant properties + // set. + if (forTitlebar) + { + if (auto acrylic = terminalBackground.try_as()) + { + winrt::WUX::Media::AcrylicBrush newBrush{}; + newBrush.TintColor(acrylic.TintColor()); + newBrush.FallbackColor(acrylic.FallbackColor()); + newBrush.TintLuminosityOpacity(acrylic.TintLuminosityOpacity()); + + // Allow acrylic opacity, but it's gotta be HostBackdrop acrylic. + // + // For now, just always use 50% opacity for this. If we do ever + // figure out how to get rid of our titlebar under the XAML tab + // row (GH#10509), we can always get rid of the HostBackdrop + // thing, and all this copying, and just return the + // terminalBackground brush directly. + // + // Because we're wholesale copying the brush, we won't be able + // to adjust it's opacity with the mouse wheel. This seems like + // an acceptable tradeoff for now. + newBrush.TintOpacity(.5); + newBrush.BackgroundSource(winrt::WUX::Media::AcrylicBackgroundSource::HostBackdrop); + return newBrush; + } + else if (auto solidColor = terminalBackground.try_as()) + { + winrt::WUX::Media::SolidColorBrush newBrush{}; + newBrush.Color(til::color{ solidColor.Color() }.with_alpha(255)); + return newBrush; + } + } + + return terminalBackground; + } + } + return nullptr; +} + +#define THEME_SETTINGS_FROM_JSON(type, name, jsonKey, ...) \ + { \ + std::optional _val; \ + _val = JsonUtils::GetValueForKey>(json, jsonKey); \ + if (_val) \ + result->name(*_val); \ + } + +#define THEME_SETTINGS_TO_JSON(type, name, jsonKey, ...) \ + JsonUtils::SetValueForKey(json, jsonKey, val.name()); + +#define THEME_OBJECT_CONVERTER(nameSpace, name, macro) \ + template<> \ + struct ::Microsoft::Terminal::Settings::Model::JsonUtils::ConversionTrait \ + { \ + nameSpace::name FromJson(const Json::Value& json) \ + { \ + if (json == Json::Value::null) \ + return nullptr; \ + auto result = winrt::make_self(); \ + macro(THEME_SETTINGS_FROM_JSON); \ + return *result; \ + } \ + \ + bool CanConvert(const Json::Value& json) \ + { \ + return json.isObject(); \ + } \ + \ + Json::Value ToJson(const nameSpace::name& val) \ + { \ + if (val == nullptr) \ + return Json::Value::null; \ + Json::Value json{ Json::ValueType::objectValue }; \ + macro(THEME_SETTINGS_TO_JSON); \ + return json; \ + } \ + \ + std::string TypeDescription() const \ + { \ + return "name (You should never see this)"; \ + } \ + }; + +THEME_OBJECT_CONVERTER(winrt::Microsoft::Terminal::Settings::Model, WindowTheme, MTSM_THEME_WINDOW_SETTINGS); +THEME_OBJECT_CONVERTER(winrt::Microsoft::Terminal::Settings::Model, TabRowTheme, MTSM_THEME_TABROW_SETTINGS); + +#undef THEME_SETTINGS_FROM_JSON +#undef THEME_SETTINGS_TO_JSON +#undef THEME_OBJECT_CONVERTER + +Theme::Theme() noexcept : + Theme{ winrt::WUX::ElementTheme::Default } +{ +} + +Theme::Theme(const winrt::WUX::ElementTheme& requestedTheme) noexcept +{ + auto window{ winrt::make_self() }; + window->RequestedTheme(requestedTheme); + _Window = *window; +} + +winrt::com_ptr Theme::Copy() const +{ + auto theme{ winrt::make_self() }; + + theme->_Name = _Name; + + if (_Window) + { + theme->_Window = *winrt::get_self(_Window)->Copy(); + } + if (_TabRow) + { + theme->_TabRow = *winrt::get_self(_TabRow)->Copy(); + } + + return theme; +} + +// Method Description: +// - Create a new instance of this class from a serialized JsonObject. +// Arguments: +// - json: an object which should be a serialization of a ColorScheme object. +// Return Value: +// - Returns nullptr for invalid JSON. +winrt::com_ptr Theme::FromJson(const Json::Value& json) +{ + auto result = winrt::make_self(); + + if (json.isString()) + { + // We found a string, not an object. Just secretly promote that string + // to a theme object with just the applicationTheme set from that value. + JsonUtils::GetValue(json, result->_Name); + winrt::WUX::ElementTheme requestedTheme{ winrt::WUX::ElementTheme::Default }; + JsonUtils::GetValue(json, requestedTheme); + + auto window{ winrt::make_self() }; + window->RequestedTheme(requestedTheme); + result->_Window = *window; + + return result; + } + + JsonUtils::GetValueForKey(json, NameKey, result->_Name); + + // This will use each of the ConversionTrait's from above to quickly parse the sub-objects + +#define THEME_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \ + { \ + std::optional _val; \ + _val = JsonUtils::GetValueForKey>(json, jsonKey); \ + if (_val) \ + result->_##name = *_val; \ + else \ + result->_##name = nullptr; \ + } + + MTSM_THEME_SETTINGS(THEME_SETTINGS_LAYER_JSON) +#undef THEME_SETTINGS_LAYER_JSON + + return result; +} + +// Method Description: +// - Create a new serialized JsonObject from an instance of this class +// Arguments: +// - +// Return Value: +// - the JsonObject representing this instance +Json::Value Theme::ToJson() const +{ + Json::Value json{ Json::ValueType::objectValue }; + + JsonUtils::SetValueForKey(json, NameKey, _Name); + + // Don't serialize anything if the object is null. +#define THEME_SETTINGS_TO_JSON(type, name, jsonKey, ...) \ + if (_##name) \ + JsonUtils::SetValueForKey(json, jsonKey, _##name); + + MTSM_THEME_SETTINGS(THEME_SETTINGS_TO_JSON) +#undef THEME_SETTINGS_TO_JSON + + return json; +} + +winrt::hstring Theme::ToString() +{ + return Name(); +} +// Method Description: +// - A helper for retrieving the RequestedTheme out of the window property. +// There's a bunch of places throughout the app that all ask for the +// RequestedTheme, this saves some hassle. If there wasn't a `window` defined +// for this theme, this'll quickly just return `system`, to use the OS theme. +// Return Value: +// - the set applicationTheme for this Theme, otherwise the system theme. +winrt::WUX::ElementTheme Theme::RequestedTheme() const noexcept +{ + return _Window ? _Window.RequestedTheme() : winrt::WUX::ElementTheme::Default; +} diff --git a/src/cascadia/TerminalSettingsModel/Theme.h b/src/cascadia/TerminalSettingsModel/Theme.h new file mode 100644 index 00000000000..4e4ba8e66be --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/Theme.h @@ -0,0 +1,107 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Theme.hpp + +Abstract: +- A Theme represents a collection of settings which control the appearance of + the Terminal window itself. Things like the color of the titlebar, the style + of the tabs. + +Author(s): +- Mike Griese - March 2022 + +--*/ +#pragma once + +#include "MTSMSettings.h" + +#include "ThemeColor.g.h" +#include "WindowTheme.g.h" +#include "TabRowTheme.g.h" +#include "Theme.g.h" + +namespace winrt::Microsoft::Terminal::Settings::Model::implementation +{ + struct ThemeColor : ThemeColorT + { + public: + ThemeColor() noexcept = default; + static winrt::Microsoft::Terminal::Settings::Model::ThemeColor FromColor(const winrt::Microsoft::Terminal::Core::Color& coreColor) noexcept; + static winrt::Microsoft::Terminal::Settings::Model::ThemeColor FromAccent() noexcept; + static winrt::Microsoft::Terminal::Settings::Model::ThemeColor FromTerminalBackground() noexcept; + + static til::color ColorFromBrush(const winrt::Windows::UI::Xaml::Media::Brush& brush); + + winrt::Windows::UI::Xaml::Media::Brush Evaluate(const winrt::Windows::UI::Xaml::ResourceDictionary& res, + const winrt::Windows::UI::Xaml::Media::Brush& terminalBackground, + const bool forTitlebar); + + WINRT_PROPERTY(til::color, Color); + WINRT_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::ThemeColorType, ColorType); + }; + +#define THEME_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \ + WINRT_PROPERTY(type, name, ##__VA_ARGS__) + +#define THEME_SETTINGS_COPY(type, name, jsonKey, ...) \ + result->_##name = _##name; + +#define COPY_THEME_OBJECT(T, macro) \ + winrt::com_ptr Copy() \ + { \ + auto result{ winrt::make_self() }; \ + macro(THEME_SETTINGS_COPY); \ + return result; \ + } + + struct WindowTheme : WindowThemeT + { + MTSM_THEME_WINDOW_SETTINGS(THEME_SETTINGS_INITIALIZE); + + public: + COPY_THEME_OBJECT(WindowTheme, MTSM_THEME_WINDOW_SETTINGS); + }; + + struct TabRowTheme : TabRowThemeT + { + MTSM_THEME_TABROW_SETTINGS(THEME_SETTINGS_INITIALIZE); + + public: + COPY_THEME_OBJECT(TabRowTheme, MTSM_THEME_TABROW_SETTINGS); + }; + + struct Theme : ThemeT + { + public: + Theme() noexcept; + Theme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) noexcept; + + com_ptr Copy() const; + + hstring ToString(); + + static com_ptr FromJson(const Json::Value& json); + void LayerJson(const Json::Value& json); + Json::Value ToJson() const; + + winrt::Windows::UI::Xaml::ElementTheme RequestedTheme() const noexcept; + + WINRT_PROPERTY(winrt::hstring, Name); + + MTSM_THEME_SETTINGS(THEME_SETTINGS_INITIALIZE) + + private: + }; + +#undef THEME_SETTINGS_INITIALIZE +#undef THEME_SETTINGS_COPY +} + +namespace winrt::Microsoft::Terminal::Settings::Model::factory_implementation +{ + BASIC_FACTORY(ThemeColor); + BASIC_FACTORY(Theme); +} diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl new file mode 100644 index 00000000000..f2932a5e1fd --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings.Model +{ + + enum ThemeColorType + { + Accent, + Color, + TerminalBackground + }; + + runtimeclass ThemeColor + { + ThemeColor(); + static ThemeColor FromColor(Microsoft.Terminal.Core.Color color); + static ThemeColor FromAccent(); + static ThemeColor FromTerminalBackground(); + + Microsoft.Terminal.Core.Color Color { get; }; + ThemeColorType ColorType; + + static Microsoft.Terminal.Core.Color ColorFromBrush(Windows.UI.Xaml.Media.Brush brush); + Windows.UI.Xaml.Media.Brush Evaluate(Windows.UI.Xaml.ResourceDictionary res, + Windows.UI.Xaml.Media.Brush terminalBackground, + Boolean forTitlebar); + } + + runtimeclass WindowTheme { + Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; + Boolean UseMica { get; }; + } + + runtimeclass TabRowTheme { + ThemeColor Background { get; }; + } + + [default_interface] runtimeclass Theme : Windows.Foundation.IStringable { + Theme(); + Theme(Windows.UI.Xaml.ElementTheme requestedTheme); + + String Name; + + // window.* Namespace + WindowTheme Window { get; }; + + // tabRow.* Namespace + TabRowTheme TabRow { get; }; + + // A helper for retrieving the RequestedTheme out of the window property + Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; + + } +} diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 3527bf125a7..530a28b6ad3 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -280,6 +280,26 @@ "brightWhite": "#EEEEEC" } ], + "themes": [ + { + "name": "light", + "window":{ + "applicationTheme": "light" + } + }, + { + "name": "dark", + "window":{ + "applicationTheme": "dark" + } + }, + { + "name": "system", + "window":{ + "applicationTheme": "system" + } + } + ], "actions": [ // Application-level Keys diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index ab496e7be4a..1f929efda20 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -405,6 +405,11 @@ void AppHost::Initialize() } }); + // Load bearing: make sure the PropertyChanged handler is added before we + // call Create, so that when the app sets up the titlebar brush, we're + // already prepared to listen for the change notification + _revokers.PropertyChanged = _logic.PropertyChanged(winrt::auto_revoke, { this, &AppHost::_PropertyChangedHandler }); + _logic.Create(); _revokers.TitleChanged = _logic.TitleChanged(winrt::auto_revoke, { this, &AppHost::AppTitleChanged }); @@ -698,8 +703,12 @@ void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspecta { if (_useNonClientArea) { - (static_cast(_window.get()))->SetTitlebarContent(arg); + auto nonClientWindow{ static_cast(_window.get()) }; + nonClientWindow->SetTitlebarContent(arg); + nonClientWindow->SetTitlebarBackground(_logic.TitlebarBrush()); } + + _updateTheme(); } // Method Description: @@ -710,9 +719,9 @@ void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspecta // - arg: the ElementTheme to use as the new theme for the UI // Return Value: // - -void AppHost::_UpdateTheme(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::ElementTheme& arg) +void AppHost::_UpdateTheme(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::ElementTheme& /*arg*/) { - _window->OnApplicationThemeChanged(arg); + _updateTheme(); } void AppHost::_FocusModeChanged(const winrt::Windows::Foundation::IInspectable&, @@ -902,8 +911,15 @@ void AppHost::_FindTargetWindow(const winrt::Windows::Foundation::IInspectable& args.ResultTargetWindowName(targetWindow.WindowName()); } -winrt::fire_and_forget AppHost::_WindowActivated() +winrt::fire_and_forget AppHost::_WindowActivated(bool activated) { + _logic.WindowActivated(activated); + + if (!activated) + { + co_return; + } + co_await winrt::resume_background(); if (auto peasant{ _windowManager.CurrentWindow() }) @@ -1326,6 +1342,25 @@ winrt::fire_and_forget AppHost::_RenameWindowRequested(const winrt::Windows::Fou } } +void AppHost::_updateTheme() +{ + auto theme = _logic.Theme(); + + _window->OnApplicationThemeChanged(theme.RequestedTheme()); + + // This block of code enables Mica for our window. By all accounts, this + // version of the code will only work on Windows 11, SV2. There's a slightly + // different API surface for enabling Mica on Windows 11 22000.0. + // + // This code is left here, commented out, for future enablement of Mica. + // We'll revisit this in GH#10509. Because we can't enable transparent + // titlebars for showing Mica currently, we're just gonna disable it + // entirely while we sort that out. + // + // const int attribute = theme.Window().UseMica() ? /*DWMSBT_MAINWINDOW*/ 2 : /*DWMSBT_NONE*/ 1; + // DwmSetWindowAttribute(_window->GetHandle(), /* DWMWA_SYSTEMBACKDROP_TYPE */ 38, &attribute, sizeof(attribute)); +} + void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { @@ -1357,6 +1392,7 @@ void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspecta } _window->SetMinimizeToNotificationAreaBehavior(_logic.GetMinimizeToNotificationArea()); + _updateTheme(); } void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, @@ -1578,3 +1614,13 @@ void AppHost::_CloseRequested(const winrt::Windows::Foundation::IInspectable& /* const auto pos = _GetWindowLaunchPosition(); _logic.CloseWindow(pos); } + +void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& e) +{ + if (e.PropertyName() == L"TitlebarBrush") + { + auto nonClientWindow{ static_cast(_window.get()) }; + nonClientWindow->SetTitlebarBackground(_logic.TitlebarBrush()); + } +} diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index a3ae4edd2ba..5c97ed3be57 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -56,7 +56,7 @@ class AppHost void _RaiseVisualBell(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& arg); void _WindowMouseWheeled(const til::point coord, const int32_t delta); - winrt::fire_and_forget _WindowActivated(); + winrt::fire_and_forget _WindowActivated(bool activated); void _WindowMoved(); void _DispatchCommandline(winrt::Windows::Foundation::IInspectable sender, @@ -122,6 +122,14 @@ class AppHost const winrt::Windows::Foundation::IInspectable& args); void _HideNotificationIconRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + + void _updateTheme(); + + void _PropertyChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, + const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + + void _initialResizeAndRepositionWindow(const HWND hwnd, RECT proposedRect, winrt::Microsoft::Terminal::Settings::Model::LaunchMode& launchMode); + std::unique_ptr _notificationIcon; winrt::event_token _ReAddNotificationIconToken; winrt::event_token _NotificationIconPressedToken; @@ -165,5 +173,6 @@ class AppHost winrt::Microsoft::Terminal::Remoting::WindowManager::ShowNotificationIconRequested_revoker ShowNotificationIconRequested; winrt::Microsoft::Terminal::Remoting::WindowManager::HideNotificationIconRequested_revoker HideNotificationIconRequested; winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; + winrt::TerminalApp::AppLogic::PropertyChanged_revoker PropertyChanged; } _revokers{}; }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 3b54950ec00..6bf1bc52b8e 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -439,10 +439,8 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize case WM_ACTIVATE: { // wparam = 0 indicates the window was deactivated - if (LOWORD(wparam) != 0) - { - _WindowActivatedHandlers(); - } + const bool activated = LOWORD(wparam) != 0; + _WindowActivatedHandlers(activated); break; } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 64e16d9397c..89bfcbc71fd 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -67,7 +67,7 @@ class IslandWindow : WINRT_CALLBACK(DragRegionClicked, winrt::delegate<>); WINRT_CALLBACK(WindowCloseButtonClicked, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); - WINRT_CALLBACK(WindowActivated, winrt::delegate); + WINRT_CALLBACK(WindowActivated, winrt::delegate); WINRT_CALLBACK(HotkeyPressed, winrt::delegate); WINRT_CALLBACK(NotifyNotificationIconPressed, winrt::delegate); WINRT_CALLBACK(NotifyWindowHidden, winrt::delegate); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 01b0bd66346..8c8f285fe3e 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -1129,3 +1129,8 @@ bool NonClientIslandWindow::_IsTitlebarVisible() const { return !(_fullscreen || _borderless); } + +void NonClientIslandWindow::SetTitlebarBackground(winrt::Windows::UI::Xaml::Media::Brush brush) +{ + _titlebar.Background(brush); +} diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 0b54ec92db3..20b9bde43ef 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -47,6 +47,8 @@ class NonClientIslandWindow : public IslandWindow void SetTitlebarContent(winrt::Windows::UI::Xaml::UIElement content); void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) override; + void SetTitlebarBackground(winrt::Windows::UI::Xaml::Media::Brush brush); + private: std::optional _oldIslandPos; diff --git a/src/cascadia/WindowsTerminal/pch.h b/src/cascadia/WindowsTerminal/pch.h index 19bfcdb27ff..d5490a62039 100644 --- a/src/cascadia/WindowsTerminal/pch.h +++ b/src/cascadia/WindowsTerminal/pch.h @@ -64,6 +64,7 @@ Module Name: #include #include #include +#include #include #include #include diff --git a/src/inc/til/color.h b/src/inc/til/color.h index fa5a141c302..5054be53318 100644 --- a/src/inc/til/color.h +++ b/src/inc/til/color.h @@ -187,14 +187,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" wss << L"#" << std::uppercase << std::setfill(L'0') << std::hex; // Force the compiler to promote from byte to int. Without it, the // stringstream will try to write the components as chars + wss << std::setw(2) << static_cast(r); + wss << std::setw(2) << static_cast(g); + wss << std::setw(2) << static_cast(b); if (!omitAlpha) { wss << std::setw(2) << static_cast(a); } - wss << std::setw(2) << static_cast(r); - wss << std::setw(2) << static_cast(g); - wss << std::setw(2) << static_cast(b); - return wss.str(); } }; diff --git a/src/types/utils.cpp b/src/types/utils.cpp index c3e432204d5..10e7661ce77 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -87,31 +87,43 @@ std::string Utils::ColorToHexString(const til::color color) // the correct format, throws E_INVALIDARG til::color Utils::ColorFromHexString(const std::string_view str) { - THROW_HR_IF(E_INVALIDARG, str.size() != 7 && str.size() != 4); + THROW_HR_IF(E_INVALIDARG, str.size() != 9 && str.size() != 7 && str.size() != 4); THROW_HR_IF(E_INVALIDARG, str.at(0) != '#'); std::string rStr; std::string gStr; std::string bStr; + std::string aStr; if (str.size() == 4) { rStr = std::string(2, str.at(1)); gStr = std::string(2, str.at(2)); bStr = std::string(2, str.at(3)); + aStr = "ff"; } - else + else if (str.size() == 7) + { + rStr = std::string(&str.at(1), 2); + gStr = std::string(&str.at(3), 2); + bStr = std::string(&str.at(5), 2); + aStr = "ff"; + } + else if (str.size() == 9) { + // #rrggbbaa rStr = std::string(&str.at(1), 2); gStr = std::string(&str.at(3), 2); bStr = std::string(&str.at(5), 2); + aStr = std::string(&str.at(7), 2); } const auto r = gsl::narrow_cast(std::stoul(rStr, nullptr, 16)); const auto g = gsl::narrow_cast(std::stoul(gStr, nullptr, 16)); const auto b = gsl::narrow_cast(std::stoul(bStr, nullptr, 16)); + const auto a = gsl::narrow_cast(std::stoul(aStr, nullptr, 16)); - return til::color{ r, g, b }; + return til::color{ r, g, b, a }; } // Routine Description: