From 07d58a800c69f5d39dbdca1612d9db3955fb32b2 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Thu, 7 Jul 2022 06:54:54 -0500 Subject: [PATCH] Initial Theme support (#12992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ##### ⚠️ targeting 1.15 ## Summary of the Pull Request Adds support for Themes, a new type of customization for the Terminal. Themes allow the user to customize elements of the Terminal window itself. In this first iteration, this PR adds support for two main properties: * enabling Mica as the window backdrop * changing the tab row color (read: changing the titelbar color) These represent the most important asks of theming in the Terminal. The properties added in this PR are: * Theme color variants: - `"#rrggbb"` or `"#aarrggbb"` - `"accent"` - `"terminalBackground"` * Properties (_listed here in dot notation, but implemented as sub-objects_) - `tabRow.background`: accepts a ThemeColor (above) - `window.applicationTheme`: accepts one of `{"system", "light", "dark"}` - `window.useMica`: accepts a boolean, defaults to false. ## References * As first described in #3327 * spec'd in #12530 ## PR Checklist * [x] Sorta enables #10509, but doesn't close it. That'll need more comprehensive changes to the titlebar code. * **update**: I totally disabled mica, but left the serialization code. It just seems silly without #10509. * [x] Closes #1963 * [x] Closes #3774 * [x] Closes #12939 * [x] Does the bulk of the #3327 work, but I'm going to leave that open since that's become my megathread for everything related to theming. * [x] I work here * [x] Tests added/passed * [ ] Requires documentation to be updated - **SURE DOES** ## Detailed Description of the Pull Request / Additional comments ### --> GO READ #12530 <-- Seriously. These themes aren't customizable in the SUI currently. You can change the active theme, and the UI will show all of the defined themes, but they're not editable there. They don't layer. You'll need to define your own themes. Thay can't come from fragments. This is a really cool future idea, but not implemented in this v0. The sub objects have some gnarly macros to generate a lot of the serialization code for you. ### TODOs * [x] I still have yet to establish what the accent color algorithm is. This might be proprietary and require a ThemeHelpers workaround. * [x] Make sure `terminalBackground` & the SUI result in something sensible * [x] Make sure runtime BG changes work with `terminalBackground`. One time, they didn't. `printf "\x1b]11;rgb:ff/00/ff\x07"` * [x] Acrylic Terminal BG's look weird, like, the opacity is always 50% or something. And the tab row looks all wrong then. ## Validation Steps Performed This is the blob I've been testing with:
```jsonc // "useAcrylicInTabRow": true, "theme": "my dark", // "theme": "Edge", "theme": "orangey", "theme": "WHITE", // "theme": "terminal", "themes": [ { "name": "my dark", "window": { "applicationTheme": "dark", "useMica": true, }, "tabRow": { "background": "#00000000", }, }, { "name": "Edge", "tabRow": { "background": "accent" }, "window": { "applicationTheme": "system" } }, { "name": "orangey", "window": { "applicationTheme": "light", "useMica": true, }, "tabRow": { "background": "#ff8800", }, }, { "name": "WHITE", "window": { "applicationTheme": "dark", "useMica": true, }, "tabRow": { "background": "#FFFFFF", }, }, { "name": "terminal", "window": { "applicationTheme": "dark", "useMica": false, }, "tabRow": { "background": "terminalBackground", }, }, ] ```
--- .github/actions/spelling/allow/apis.txt | 5 + .../SettingsModel.LocalTests.vcxproj | 1 + .../LocalTests_SettingsModel/ThemeTests.cpp | 276 +++++++++++++++ src/cascadia/TerminalApp/AppLogic.cpp | 40 ++- src/cascadia/TerminalApp/AppLogic.h | 8 + src/cascadia/TerminalApp/AppLogic.idl | 6 +- src/cascadia/TerminalApp/HighlightedText.h | 2 - .../Resources/en-US/Resources.resw | 4 + src/cascadia/TerminalApp/TabManagement.cpp | 4 + src/cascadia/TerminalApp/TerminalPage.cpp | 242 ++++++++----- src/cascadia/TerminalApp/TerminalPage.h | 9 +- src/cascadia/TerminalApp/TerminalPage.idl | 3 + src/cascadia/TerminalApp/TitlebarControl.xaml | 1 - src/cascadia/TerminalControl/TermControl.cpp | 16 + src/cascadia/TerminalControl/TermControl.h | 4 + src/cascadia/TerminalControl/TermControl.idl | 4 +- .../GlobalAppearance.cpp | 66 +++- .../TerminalSettingsEditor/GlobalAppearance.h | 10 +- .../GlobalAppearance.idl | 3 +- .../GlobalAppearance.xaml | 10 +- .../TerminalSettingsEditor/MainPage.cpp | 6 + .../TerminalSettingsEditor/MainPage.h | 2 + .../TerminalSettingsEditor/MainPage.idl | 2 + .../CascadiaSettings.cpp | 12 + .../TerminalSettingsModel/CascadiaSettings.h | 2 + .../CascadiaSettingsSerialization.cpp | 37 +- .../GlobalAppSettings.cpp | 32 ++ .../TerminalSettingsModel/GlobalAppSettings.h | 7 + .../GlobalAppSettings.idl | 7 +- .../TerminalSettingsModel/MTSMSettings.h | 13 +- ...crosoft.Terminal.Settings.ModelLib.vcxproj | 7 + .../TerminalSettingsSerializationHelpers.h | 73 ++++ .../TerminalWarnings.idl | 1 + src/cascadia/TerminalSettingsModel/Theme.cpp | 331 ++++++++++++++++++ src/cascadia/TerminalSettingsModel/Theme.h | 107 ++++++ src/cascadia/TerminalSettingsModel/Theme.idl | 55 +++ .../TerminalSettingsModel/defaults.json | 20 ++ src/cascadia/WindowsTerminal/AppHost.cpp | 54 ++- src/cascadia/WindowsTerminal/AppHost.h | 11 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 6 +- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 5 + .../WindowsTerminal/NonClientIslandWindow.h | 2 + src/cascadia/WindowsTerminal/pch.h | 1 + src/inc/til/color.h | 7 +- src/types/utils.cpp | 18 +- 46 files changed, 1398 insertions(+), 136 deletions(-) create mode 100644 src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp create mode 100644 src/cascadia/TerminalSettingsModel/Theme.cpp create mode 100644 src/cascadia/TerminalSettingsModel/Theme.h create mode 100644 src/cascadia/TerminalSettingsModel/Theme.idl diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index d44c2ddf83..0b8c0d91a9 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 4721427072..76e0f07e29 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 0000000000..25e115a0cd --- /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 8f58309ff5..4a05ef2f21 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 63d1c4e784..2ba2deaeff 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 a5c0eb05a7..42096bd08d 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 e71c7ef752..c9c29e7cbb 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 a0a653927e..29e70316f8 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 47cfa70d7b..d88b7a7b4f 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 197cbce47c..a975a7f614 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 310910f522..f4e8091dd9 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 90d317c1d9..89ea3ec327 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 fde78d3a81..e5eda4f504 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 7a1b57df3a..3122093d19 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 e67f7c773c..655fce03fd 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 3d8cde2714..101359b30e 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 b34bf0a1e9..1c6032b832 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 7e48178baf..3da730eb6c 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 7e3483140a..a45ba503fc 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 2d15f1c210..980b5323ac 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 2009502c56..45ac57129e 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 2f7d5388cd..ac257da0be 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 4c77141975..2b724ab67b 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 8a5b98ea5c..4154786c30 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 737fa8075a..c6141df700 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 c8baba43e8..f8e4dc4f80 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 e417042e44..64ddea29f0 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 14584b26a7..3c876c75d3 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 51386e6be3..c0226eaab6 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 e425bdfd87..259c22951b 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 21184811d3..678446dff7 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 9dd273ee3c..75e3fb63c1 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 c8aea6b95a..43dee4e5a6 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 0000000000..e4b770a6ce --- /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 0000000000..4e4ba8e66b --- /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 0000000000..f2932a5e1f --- /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 3527bf125a..530a28b6ad 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 ab496e7be4..1f929efda2 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 a3ae4edd2b..5c97ed3be5 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -56,7 +56,7 @@ private: 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 @@ private: 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 @@ private: 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 3b54950ec0..6bf1bc52b8 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 64e16d9397..89bfcbc71f 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -67,7 +67,7 @@ public: 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 01b0bd6634..8c8f285fe3 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 0b54ec92db..20b9bde43e 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -47,6 +47,8 @@ public: 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 19bfcdb27f..d5490a6203 100644 --- a/src/cascadia/WindowsTerminal/pch.h +++ b/src/cascadia/WindowsTerminal/pch.h @@ -64,6 +64,7 @@ Abstract: #include #include #include +#include #include #include #include diff --git a/src/inc/til/color.h b/src/inc/til/color.h index fa5a141c30..5054be5331 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 c3e432204d..10e7661ce7 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: