diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index 2f025073f97..7dabb689a3c 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -153,6 +153,9 @@ + + @@ -166,6 +169,9 @@ + + @@ -183,6 +189,9 @@ --> + + diff --git a/src/cascadia/TerminalApp/SettingsTab.cpp b/src/cascadia/TerminalApp/SettingsTab.cpp index 54e48ed1e0d..6ac9bb5e79e 100644 --- a/src/cascadia/TerminalApp/SettingsTab.cpp +++ b/src/cascadia/TerminalApp/SettingsTab.cpp @@ -23,9 +23,11 @@ namespace winrt namespace winrt::TerminalApp::implementation { - SettingsTab::SettingsTab(MainPage settingsUI) + SettingsTab::SettingsTab(MainPage settingsUI, + winrt::Windows::UI::Xaml::ElementTheme requestedTheme) { Content(settingsUI); + _requestedTheme = requestedTheme; _MakeTabViewItem(); _CreateContextMenu(); @@ -36,6 +38,10 @@ namespace winrt::TerminalApp::implementation { auto settingsUI{ Content().as() }; settingsUI.UpdateSettings(settings); + + // Stash away the current requested theme of the app. We'll need that in + // _BackgroundBrush() to do a theme-aware resource lookup + _requestedTheme = settings.GlobalSettings().CurrentTheme().RequestedTheme(); } // Method Description: @@ -105,4 +111,16 @@ namespace winrt::TerminalApp::implementation TabViewItem().IconSource(IconPathConverter::IconSourceMUX(glyph)); } } + + winrt::Windows::UI::Xaml::Media::Brush SettingsTab::_BackgroundBrush() + { + // Look up the color we should use for the settings tab item from our + // resources. This should only be used for when "terminalBackground" is + // requested. + static const auto key = winrt::box_value(L"SettingsUiTabBrush"); + // You can't just do a Application::Current().Resources().TryLookup + // lookup, cause the app theme never changes! Do the hacky version + // instead. + return ThemeLookup(Application::Current().Resources(), _requestedTheme, key).try_as(); + } } diff --git a/src/cascadia/TerminalApp/SettingsTab.h b/src/cascadia/TerminalApp/SettingsTab.h index 803ed8cb03e..e9cfc9c8376 100644 --- a/src/cascadia/TerminalApp/SettingsTab.h +++ b/src/cascadia/TerminalApp/SettingsTab.h @@ -24,7 +24,8 @@ namespace winrt::TerminalApp::implementation struct SettingsTab : SettingsTabT { public: - SettingsTab(winrt::Microsoft::Terminal::Settings::Editor::MainPage settingsUI); + SettingsTab(winrt::Microsoft::Terminal::Settings::Editor::MainPage settingsUI, + winrt::Windows::UI::Xaml::ElementTheme requestedTheme); void UpdateSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings); void Focus(winrt::Windows::UI::Xaml::FocusState focusState) override; @@ -32,7 +33,11 @@ namespace winrt::TerminalApp::implementation std::vector BuildStartupActions() const override; private: + winrt::Windows::UI::Xaml::ElementTheme _requestedTheme; + void _MakeTabViewItem() override; winrt::fire_and_forget _CreateIcon(); + + virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override; }; } diff --git a/src/cascadia/TerminalApp/TabBase.cpp b/src/cascadia/TerminalApp/TabBase.cpp index 167b5008066..dda15f10bc8 100644 --- a/src/cascadia/TerminalApp/TabBase.cpp +++ b/src/cascadia/TerminalApp/TabBase.cpp @@ -5,6 +5,8 @@ #include #include "TabBase.h" #include "TabBase.g.cpp" +#include "Utils.h" +#include "ColorHelper.h" using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -252,4 +254,291 @@ namespace winrt::TerminalApp::implementation }); } + std::optional TabBase::GetTabColor() + { + return std::nullopt; + } + + void TabBase::ThemeColor(const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& focused, + const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& unfocused, + const til::color& tabRowColor) + { + _themeColor = focused; + _unfocusedThemeColor = unfocused; + _tabRowColor = tabRowColor; + _RecalculateAndApplyTabColor(); + } + + // Method Description: + // - This function dispatches a function to the UI thread to recalculate + // what this tab's current background color should be. If a color is set, + // it will apply the given color to the tab's background. Otherwise, it + // will clear the tab's background color. + // Arguments: + // - + // Return Value: + // - + void TabBase::_RecalculateAndApplyTabColor() + { + auto weakThis{ get_weak() }; + + TabViewItem().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis]() { + auto ptrTab = weakThis.get(); + if (!ptrTab) + { + return; + } + + auto tab{ ptrTab }; + + // GetTabColor will return the color set by the color picker, or the + // color specified in the profile. If neither of those were set, + // then look to _themeColor to see if there's a value there. + // Otherwise, clear our color, falling back to the TabView defaults. + const auto currentColor = tab->GetTabColor(); + if (currentColor.has_value()) + { + tab->_ApplyTabColorOnUIThread(currentColor.value()); + } + else if (tab->_themeColor != nullptr) + { + // Safely get the active control's brush. + const Media::Brush terminalBrush{ tab->_BackgroundBrush() }; + + if (const auto themeBrush{ tab->_themeColor.Evaluate(Application::Current().Resources(), terminalBrush, false) }) + { + // ThemeColor.Evaluate will get us a Brush (because the + // TermControl could have an acrylic BG, for example). Take + // that brush, and get the color out of it. We don't really + // want to have the tab items themselves be acrylic. + tab->_ApplyTabColorOnUIThread(til::color{ ThemeColor::ColorFromBrush(themeBrush) }); + } + else + { + tab->_ClearTabBackgroundColor(); + } + } + else + { + tab->_ClearTabBackgroundColor(); + } + }); + } + + // Method Description: + // - Applies the given color to the background of this tab's TabViewItem. + // - Sets the tab foreground color depending on the luminance of + // the background color + // - This method should only be called on the UI thread. + // Arguments: + // - color: the color the user picked for their tab + // Return Value: + // - + void TabBase::_ApplyTabColorOnUIThread(const winrt::Windows::UI::Color& color) + { + Media::SolidColorBrush selectedTabBrush{}; + Media::SolidColorBrush deselectedTabBrush{}; + Media::SolidColorBrush fontBrush{}; + Media::SolidColorBrush deselectedFontBrush{}; + Media::SolidColorBrush secondaryFontBrush{}; + Media::SolidColorBrush hoverTabBrush{}; + Media::SolidColorBrush subtleFillColorSecondaryBrush; + Media::SolidColorBrush subtleFillColorTertiaryBrush; + // calculate the luminance of the current color and select a font + // color based on that + // see https://www.w3.org/TR/WCAG20/#relativeluminancedef + if (TerminalApp::ColorHelper::IsBrightColor(color)) + { + fontBrush.Color(winrt::Windows::UI::Colors::Black()); + auto secondaryFontColor = winrt::Windows::UI::Colors::Black(); + // For alpha value see: https://github.com/microsoft/microsoft-ui-xaml/blob/7a33ad772d77d908aa6b316ec24e6d2eb3ebf571/dev/CommonStyles/Common_themeresources_any.xaml#L269 + secondaryFontColor.A = 0x9E; + secondaryFontBrush.Color(secondaryFontColor); + auto subtleFillColorSecondary = winrt::Windows::UI::Colors::Black(); + subtleFillColorSecondary.A = 0x09; + subtleFillColorSecondaryBrush.Color(subtleFillColorSecondary); + auto subtleFillColorTertiary = winrt::Windows::UI::Colors::Black(); + subtleFillColorTertiary.A = 0x06; + subtleFillColorTertiaryBrush.Color(subtleFillColorTertiary); + } + else + { + fontBrush.Color(winrt::Windows::UI::Colors::White()); + auto secondaryFontColor = winrt::Windows::UI::Colors::White(); + // For alpha value see: https://github.com/microsoft/microsoft-ui-xaml/blob/7a33ad772d77d908aa6b316ec24e6d2eb3ebf571/dev/CommonStyles/Common_themeresources_any.xaml#L14 + secondaryFontColor.A = 0xC5; + secondaryFontBrush.Color(secondaryFontColor); + auto subtleFillColorSecondary = winrt::Windows::UI::Colors::White(); + subtleFillColorSecondary.A = 0x0F; + subtleFillColorSecondaryBrush.Color(subtleFillColorSecondary); + auto subtleFillColorTertiary = winrt::Windows::UI::Colors::White(); + subtleFillColorTertiary.A = 0x0A; + subtleFillColorTertiaryBrush.Color(subtleFillColorTertiary); + } + + selectedTabBrush.Color(color); + + // Start with the current tab color, set to Opacity=.3 + til::color deselectedTabColor{ color }; + deselectedTabColor = deselectedTabColor.with_alpha(77); // 255 * .3 = 77 + + // If we DON'T have a color set from the color picker, or the profile's + // tabColor, but we do have a unfocused color in the theme, use the + // unfocused theme color here instead. + if (!GetTabColor().has_value() && + _unfocusedThemeColor != nullptr) + { + // Safely get the active control's brush. + const Media::Brush terminalBrush{ _BackgroundBrush() }; + + // Get the color of the brush. + if (const auto themeBrush{ _unfocusedThemeColor.Evaluate(Application::Current().Resources(), terminalBrush, false) }) + { + // We did figure out the brush. Get the color out of it. If it + // was "accent" or "terminalBackground", then we're gonna set + // the alpha to .3 manually here. + // (ThemeColor::UnfocusedTabOpacity will do this for us). If the + // user sets both unfocused and focused tab.background to + // terminalBackground, this will allow for some differentiation + // (and is generally just sensible). + deselectedTabColor = til::color{ ThemeColor::ColorFromBrush(themeBrush) }.with_alpha(_unfocusedThemeColor.UnfocusedTabOpacity()); + } + } + + // currently if a tab has a custom color, a deselected state is + // signified by using the same color with a bit of transparency + deselectedTabBrush.Color(deselectedTabColor.with_alpha(255)); + deselectedTabBrush.Opacity(deselectedTabColor.a / 255.f); + + hoverTabBrush.Color(color); + hoverTabBrush.Opacity(0.6); + + // Account for the color of the tab row when setting the color of text + // on inactive tabs. Consider: + // * black active tabs + // * on a white tab row + // * with a transparent inactive tab color + // + // We don't want that to result in white text on a white tab row for + // inactive tabs. + const auto deselectedActualColor = deselectedTabColor.layer_over(_tabRowColor); + if (TerminalApp::ColorHelper::IsBrightColor(deselectedActualColor)) + { + deselectedFontBrush.Color(winrt::Windows::UI::Colors::Black()); + } + else + { + deselectedFontBrush.Color(winrt::Windows::UI::Colors::White()); + } + + // Prior to MUX 2.7, we set TabViewItemHeaderBackground, but now we can + // use TabViewItem().Background() for that. HOWEVER, + // TabViewItem().Background() only sets the color of the tab background + // when the TabViewItem is unselected. So we still need to set the other + // properties ourselves. + // + // In GH#11294 we thought we'd still need to set + // TabViewItemHeaderBackground manually, but GH#11382 discovered that + // Background() was actually okay after all. + TabViewItem().Background(deselectedTabBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); + + // Similarly, TabViewItem().Foreground() sets the color for the text + // when the TabViewItem isn't selected, but not when it is hovered, + // pressed, dragged, or selected, so we'll need to just set them all + // anyways. + TabViewItem().Foreground(deselectedFontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForeground"), deselectedFontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundSelected"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPointerOver"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPressed"), fontBrush); + + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForeground"), deselectedFontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPressed"), secondaryFontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPointerOver"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPressedCloseButtonForeground"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPointerOverCloseButtonForeground"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderSelectedCloseButtonForeground"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPressed"), subtleFillColorTertiaryBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPointerOver"), subtleFillColorSecondaryBrush); + + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundActiveTab"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPressed"), fontBrush); + TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPointerOver"), fontBrush); + + _RefreshVisualState(); + } + + // Method Description: + // - Clear out any color we've set for the TabViewItem. + // - This method should only be called on the UI thread. + // Arguments: + // - + // Return Value: + // - + void TabBase::_ClearTabBackgroundColor() + { + static const winrt::hstring keys[] = { + L"TabViewItemHeaderBackground", + L"TabViewItemHeaderBackgroundSelected", + L"TabViewItemHeaderBackgroundPointerOver", + L"TabViewItemHeaderBackgroundPressed", + L"TabViewItemHeaderForeground", + L"TabViewItemHeaderForegroundSelected", + L"TabViewItemHeaderForegroundPointerOver", + L"TabViewItemHeaderForegroundPressed", + L"TabViewItemHeaderCloseButtonForeground", + L"TabViewItemHeaderCloseButtonForegroundPressed", + L"TabViewItemHeaderCloseButtonForegroundPointerOver", + L"TabViewItemHeaderPressedCloseButtonForeground", + L"TabViewItemHeaderPointerOverCloseButtonForeground", + L"TabViewItemHeaderSelectedCloseButtonForeground", + L"TabViewItemHeaderCloseButtonBackgroundPressed", + L"TabViewItemHeaderCloseButtonBackgroundPointerOver", + L"TabViewButtonForegroundActiveTab", + L"TabViewButtonForegroundPressed", + L"TabViewButtonForegroundPointerOver" + }; + + // simply clear any of the colors in the tab's dict + for (const auto& keyString : keys) + { + auto key = winrt::box_value(keyString); + if (TabViewItem().Resources().HasKey(key)) + { + TabViewItem().Resources().Remove(key); + } + } + + // GH#11382 DON'T set the background to null. If you do that, then the + // tab won't be hit testable at all. Transparent, however, is a totally + // valid hit test target. That makes sense. + TabViewItem().Background(WUX::Media::SolidColorBrush{ Windows::UI::Colors::Transparent() }); + + _RefreshVisualState(); + } + + // Method Description: + // Toggles the visual state of the tab view item, + // so that changes to the tab color are reflected immediately + // Arguments: + // - + // Return Value: + // - + void TabBase::_RefreshVisualState() + { + if (TabViewItem().IsSelected()) + { + VisualStateManager::GoToState(TabViewItem(), L"Normal", true); + VisualStateManager::GoToState(TabViewItem(), L"Selected", true); + } + else + { + VisualStateManager::GoToState(TabViewItem(), L"Selected", true); + VisualStateManager::GoToState(TabViewItem(), L"Normal", true); + } + } + } diff --git a/src/cascadia/TerminalApp/TabBase.h b/src/cascadia/TerminalApp/TabBase.h index a04873f8905..fb32fe0e377 100644 --- a/src/cascadia/TerminalApp/TabBase.h +++ b/src/cascadia/TerminalApp/TabBase.h @@ -25,6 +25,11 @@ namespace winrt::TerminalApp::implementation void SetActionMap(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap); virtual std::vector BuildStartupActions() const = 0; + virtual std::optional GetTabColor(); + void ThemeColor(const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& focused, + const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& unfocused, + const til::color& tabRowColor); + WINRT_CALLBACK(RequestFocusActiveControl, winrt::delegate); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); @@ -51,6 +56,10 @@ namespace winrt::TerminalApp::implementation Microsoft::Terminal::Settings::Model::IActionMapView _actionMap{ nullptr }; winrt::hstring _keyChord{}; + winrt::Microsoft::Terminal::Settings::Model::ThemeColor _themeColor{ nullptr }; + winrt::Microsoft::Terminal::Settings::Model::ThemeColor _unfocusedThemeColor{ nullptr }; + til::color _tabRowColor; + virtual void _CreateContextMenu(); virtual winrt::hstring _CreateToolTipTitle(); @@ -63,6 +72,12 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget _UpdateSwitchToTabKeyChord(); void _UpdateToolTip(); + void _RecalculateAndApplyTabColor(); + void _ApplyTabColorOnUIThread(const winrt::Windows::UI::Color& color); + void _ClearTabBackgroundColor(); + void _RefreshVisualState(); + virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() = 0; + friend class ::TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 36937f4481d..37436c48cc3 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -19,6 +19,7 @@ #include "DebugTapConnection.h" #include "SettingsTab.h" #include "TabRowControl.h" +#include "Utils.h" using namespace winrt; using namespace winrt::Microsoft::Terminal::Control; @@ -3321,7 +3322,7 @@ namespace winrt::TerminalApp::implementation } }); - auto newTabImpl = winrt::make_self(sui); + auto newTabImpl = winrt::make_self(sui, _settings.GlobalSettings().CurrentTheme().RequestedTheme()); // Add the new tab to the list of our tabs. _tabs.Append(*newTabImpl); @@ -4101,79 +4102,11 @@ namespace winrt::TerminalApp::implementation const auto theme = _settings.GlobalSettings().CurrentTheme(); auto requestedTheme{ theme.RequestedTheme() }; - // First: Update the colors of our individual TabViewItems. This applies tab.background to the tabs via TerminalTab::ThemeColor - { - auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; - for (const auto& tab : _tabs) - { - if (const auto& terminalTabImpl{ _GetTerminalTabImpl(tab) }) - { - terminalTabImpl->ThemeColor(tabBackground); - } - } - } - 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(); + const auto backgroundSolidBrush = ThemeLookup(res, requestedTheme, tabViewBackgroundKey).as(); til::color bgColor = backgroundSolidBrush.Color(); @@ -4218,6 +4151,21 @@ namespace winrt::TerminalApp::implementation _tabRow.Background(TitlebarBrush()); } + // Second: Update the colors of our individual TabViewItems. This + // applies tab.background to the tabs via TerminalTab::ThemeColor. + // + // Do this second, so that we already know the bgColor of the titlebar. + { + const auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; + const auto tabUnfocusedBackground = theme.Tab() ? theme.Tab().UnfocusedBackground() : nullptr; + for (const auto& tab : _tabs) + { + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tab)); + tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor); + } + } + // 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. diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 6ef8c735853..4bac51928bf 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -1394,169 +1394,6 @@ namespace winrt::TerminalApp::implementation _RecalculateAndApplyTabColor(); } - void TerminalTab::ThemeColor(const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& color) - { - _themeColor = color; - _RecalculateAndApplyTabColor(); - } - - // Method Description: - // - This function dispatches a function to the UI thread to recalculate - // what this tab's current background color should be. If a color is set, - // it will apply the given color to the tab's background. Otherwise, it - // will clear the tab's background color. - // Arguments: - // - - // Return Value: - // - - void TerminalTab::_RecalculateAndApplyTabColor() - { - auto weakThis{ get_weak() }; - - TabViewItem().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis]() { - auto ptrTab = weakThis.get(); - if (!ptrTab) - return; - - auto tab{ ptrTab }; - - // GetTabColor will return the color set by the color picker, or the - // color specified in the profile. If neither of those were set, - // then look to _themeColor to see if there's a value there. - // Otherwise, clear our color, falling back to the TabView defaults. - auto currentColor = tab->GetTabColor(); - if (currentColor.has_value()) - { - tab->_ApplyTabColor(currentColor.value()); - } - else if (tab->_themeColor != nullptr) - { - // One-liner to safely get the active control's brush. - Media::Brush terminalBrush{ nullptr }; - if (const auto& c{ tab->GetActiveTerminalControl() }) - { - terminalBrush = c.BackgroundBrush(); - } - - if (const auto themeBrush{ tab->_themeColor.Evaluate(Application::Current().Resources(), terminalBrush, false) }) - { - // ThemeColor.Evaluate will get us a Brush (because the - // TermControl could have an acrylic BG, for example). Take - // that brush, and get the color out of it. We don't really - // want to have the tab items themselves be acrylic. - tab->_ApplyTabColor(til::color{ ThemeColor::ColorFromBrush(themeBrush) }); - } - else - { - tab->_ClearTabBackgroundColor(); - } - } - else - { - tab->_ClearTabBackgroundColor(); - } - }); - } - - // Method Description: - // - Applies the given color to the background of this tab's TabViewItem. - // - Sets the tab foreground color depending on the luminance of - // the background color - // - This method should only be called on the UI thread. - // Arguments: - // - color: the color the user picked for their tab - // Return Value: - // - - void TerminalTab::_ApplyTabColor(const winrt::Windows::UI::Color& color) - { - Media::SolidColorBrush selectedTabBrush{}; - Media::SolidColorBrush deselectedTabBrush{}; - Media::SolidColorBrush fontBrush{}; - Media::SolidColorBrush secondaryFontBrush{}; - Media::SolidColorBrush hoverTabBrush{}; - Media::SolidColorBrush subtleFillColorSecondaryBrush; - Media::SolidColorBrush subtleFillColorTertiaryBrush; - // calculate the luminance of the current color and select a font - // color based on that - // see https://www.w3.org/TR/WCAG20/#relativeluminancedef - if (TerminalApp::ColorHelper::IsBrightColor(color)) - { - fontBrush.Color(winrt::Windows::UI::Colors::Black()); - auto secondaryFontColor = winrt::Windows::UI::Colors::Black(); - // For alpha value see: https://github.com/microsoft/microsoft-ui-xaml/blob/7a33ad772d77d908aa6b316ec24e6d2eb3ebf571/dev/CommonStyles/Common_themeresources_any.xaml#L269 - secondaryFontColor.A = 0x9E; - secondaryFontBrush.Color(secondaryFontColor); - auto subtleFillColorSecondary = winrt::Windows::UI::Colors::Black(); - subtleFillColorSecondary.A = 0x09; - subtleFillColorSecondaryBrush.Color(subtleFillColorSecondary); - auto subtleFillColorTertiary = winrt::Windows::UI::Colors::Black(); - subtleFillColorTertiary.A = 0x06; - subtleFillColorTertiaryBrush.Color(subtleFillColorTertiary); - } - else - { - fontBrush.Color(winrt::Windows::UI::Colors::White()); - auto secondaryFontColor = winrt::Windows::UI::Colors::White(); - // For alpha value see: https://github.com/microsoft/microsoft-ui-xaml/blob/7a33ad772d77d908aa6b316ec24e6d2eb3ebf571/dev/CommonStyles/Common_themeresources_any.xaml#L14 - secondaryFontColor.A = 0xC5; - secondaryFontBrush.Color(secondaryFontColor); - auto subtleFillColorSecondary = winrt::Windows::UI::Colors::White(); - subtleFillColorSecondary.A = 0x0F; - subtleFillColorSecondaryBrush.Color(subtleFillColorSecondary); - auto subtleFillColorTertiary = winrt::Windows::UI::Colors::White(); - subtleFillColorTertiary.A = 0x0A; - subtleFillColorTertiaryBrush.Color(subtleFillColorTertiary); - } - - selectedTabBrush.Color(color); - - // currently if a tab has a custom color, a deselected state is - // signified by using the same color with a bit of transparency - deselectedTabBrush.Color(color); - deselectedTabBrush.Opacity(0.3); - - hoverTabBrush.Color(color); - hoverTabBrush.Opacity(0.6); - - // Prior to MUX 2.7, we set TabViewItemHeaderBackground, but now we can - // use TabViewItem().Background() for that. HOWEVER, - // TabViewItem().Background() only sets the color of the tab background - // when the TabViewItem is unselected. So we still need to set the other - // properties ourselves. - // - // In GH#11294 we thought we'd still need to set - // TabViewItemHeaderBackground manually, but GH#11382 discovered that - // Background() was actually okay after all. - TabViewItem().Background(deselectedTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); - - // TabViewItem().Foreground() unfortunately does not work for us. It - // sets the color for the text when the TabViewItem isn't selected, but - // not when it is hovered, pressed, dragged, or selected, so we'll need - // to just set them all anyways. - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundSelected"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPointerOver"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPressed"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPressed"), secondaryFontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonForegroundPointerOver"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPressedCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderPointerOverCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderSelectedCloseButtonForeground"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPressed"), subtleFillColorTertiaryBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewItemHeaderCloseButtonBackgroundPointerOver"), subtleFillColorSecondaryBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundActiveTab"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPressed"), fontBrush); - TabViewItem().Resources().Insert(winrt::box_value(L"TabViewButtonForegroundPointerOver"), fontBrush); - - _RefreshVisualState(); - - _ColorSelectedHandlers(color); - } - // Method Description: // - Clear the custom runtime color of the tab, if any color is set. This // will re-apply whatever the tab's base color should be (either the color @@ -1571,54 +1408,14 @@ namespace winrt::TerminalApp::implementation _RecalculateAndApplyTabColor(); } - // Method Description: - // - Clear out any color we've set for the TabViewItem. - // - This method should only be called on the UI thread. - // Arguments: - // - - // Return Value: - // - - void TerminalTab::_ClearTabBackgroundColor() + winrt::Windows::UI::Xaml::Media::Brush TerminalTab::_BackgroundBrush() { - winrt::hstring keys[] = { - L"TabViewItemHeaderBackground", - L"TabViewItemHeaderBackgroundSelected", - L"TabViewItemHeaderBackgroundPointerOver", - L"TabViewItemHeaderBackgroundPressed", - L"TabViewItemHeaderForeground", - L"TabViewItemHeaderForegroundSelected", - L"TabViewItemHeaderForegroundPointerOver", - L"TabViewItemHeaderForegroundPressed", - L"TabViewItemHeaderCloseButtonForeground", - L"TabViewItemHeaderCloseButtonForegroundPressed", - L"TabViewItemHeaderCloseButtonForegroundPointerOver", - L"TabViewItemHeaderPressedCloseButtonForeground", - L"TabViewItemHeaderPointerOverCloseButtonForeground", - L"TabViewItemHeaderSelectedCloseButtonForeground", - L"TabViewItemHeaderCloseButtonBackgroundPressed", - L"TabViewItemHeaderCloseButtonBackgroundPointerOver", - L"TabViewButtonForegroundActiveTab", - L"TabViewButtonForegroundPressed", - L"TabViewButtonForegroundPointerOver" - }; - - // simply clear any of the colors in the tab's dict - for (auto keyString : keys) + Media::Brush terminalBrush{ nullptr }; + if (const auto& c{ GetActiveTerminalControl() }) { - auto key = winrt::box_value(keyString); - if (TabViewItem().Resources().HasKey(key)) - { - TabViewItem().Resources().Remove(key); - } + terminalBrush = c.BackgroundBrush(); } - - // GH#11382 DON'T set the background to null. If you do that, then the - // tab won't be hit testable at all. Transparent, however, is a totally - // valid hit test target. That makes sense. - TabViewItem().Background(WUX::Media::SolidColorBrush{ Windows::UI::Colors::Transparent() }); - - _RefreshVisualState(); - _ColorClearedHandlers(); + return terminalBrush; } // Method Description: @@ -1633,27 +1430,6 @@ namespace winrt::TerminalApp::implementation _ColorPickerRequestedHandlers(); } - // Method Description: - // Toggles the visual state of the tab view item, - // so that changes to the tab color are reflected immediately - // Arguments: - // - - // Return Value: - // - - void TerminalTab::_RefreshVisualState() - { - if (TabViewItem().IsSelected()) - { - VisualStateManager::GoToState(TabViewItem(), L"Normal", true); - VisualStateManager::GoToState(TabViewItem(), L"Selected", true); - } - else - { - VisualStateManager::GoToState(TabViewItem(), L"Selected", true); - VisualStateManager::GoToState(TabViewItem(), L"Normal", true); - } - } - // - Get the total number of leaf panes in this tab. This will be the number // of actual controls hosted by this tab. // Arguments: diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 5f7a4c8ed82..53492095db1 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -71,9 +71,7 @@ namespace winrt::TerminalApp::implementation void ResetTabText(); void ActivateTabRenamer(); - std::optional GetTabColor(); - - void ThemeColor(const winrt::Microsoft::Terminal::Settings::Model::ThemeColor& color); + virtual std::optional GetTabColor() override; void SetRuntimeTabColor(const winrt::Windows::UI::Color& color); void ResetRuntimeTabColor(); void RequestColorPicker(); @@ -100,8 +98,6 @@ namespace winrt::TerminalApp::implementation } WINRT_CALLBACK(ActivePaneChanged, winrt::delegate<>); - WINRT_CALLBACK(ColorSelected, winrt::delegate); - WINRT_CALLBACK(ColorCleared, winrt::delegate<>); WINRT_CALLBACK(TabRaiseVisualBell, winrt::delegate<>); WINRT_CALLBACK(DuplicateRequested, winrt::delegate<>); WINRT_CALLBACK(SplitTabRequested, winrt::delegate<>); @@ -119,7 +115,6 @@ namespace winrt::TerminalApp::implementation std::optional _runtimeTabColor{}; winrt::TerminalApp::TabHeaderControl _headerControl{}; winrt::TerminalApp::TerminalTabStatus _tabStatus{}; - winrt::Microsoft::Terminal::Settings::Model::ThemeColor _themeColor{ nullptr }; winrt::TerminalApp::ColorPickupFlyout _tabColorPickup{ nullptr }; winrt::event_token _colorSelectedToken; @@ -163,8 +158,6 @@ namespace winrt::TerminalApp::implementation void _CreateContextMenu() override; virtual winrt::hstring _CreateToolTipTitle() override; - void _RefreshVisualState(); - void _DetachEventHandlersFromControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); void _AttachEventHandlersToControl(const uint32_t paneId, const winrt::Microsoft::Terminal::Control::TermControl& control); void _AttachEventHandlersToPane(std::shared_ptr pane); @@ -173,16 +166,14 @@ namespace winrt::TerminalApp::implementation winrt::hstring _GetActiveTitle() const; - void _RecalculateAndApplyTabColor(); - void _ApplyTabColor(const winrt::Windows::UI::Color& color); - void _ClearTabBackgroundColor(); - void _RecalculateAndApplyReadOnly(); void _UpdateProgressState(); void _DuplicateTab(); + virtual winrt::Windows::UI::Xaml::Media::Brush _BackgroundBrush() override; + friend class ::TerminalAppLocalTests::TabTests; }; } diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 4ebc362ddd6..502b4adb404 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -129,6 +129,7 @@ Author(s): X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) -#define MTSM_THEME_TAB_SETTINGS(X) \ - X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ +#define MTSM_THEME_TAB_SETTINGS(X) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Background, "background", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \ X(winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility, ShowCloseButton, "showCloseButton", winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always) diff --git a/src/cascadia/TerminalSettingsModel/Theme.cpp b/src/cascadia/TerminalSettingsModel/Theme.cpp index dfb108634ac..c7236a8463f 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.cpp +++ b/src/cascadia/TerminalSettingsModel/Theme.cpp @@ -199,6 +199,30 @@ winrt::WUX::Media::Brush ThemeColor::Evaluate(const winrt::WUX::ResourceDictiona return nullptr; } +// Method Description: +// - This is not an actual property on a theme color setting, but rather +// something derived from the value itself. This is "the opacity we should use +// for this ThemeColor should it be used as a unfocusedTab color". Basically, +// terminalBackground and accent use 30% opacity when set, to match the how +// inactive tabs were colored before themes existed. +// Arguments: +// - +// Return Value: +// - the opacity that should be used if this color is being applied to a +// tab.unfocusedBackground property. +uint8_t ThemeColor::UnfocusedTabOpacity() const noexcept +{ + switch (ColorType()) + { + case ThemeColorType::Accent: + case ThemeColorType::TerminalBackground: + return 77; // 77 = .3 * 256 + case ThemeColorType::Color: + return _Color.a; + } + return 0; +} + #define THEME_SETTINGS_FROM_JSON(type, name, jsonKey, ...) \ { \ std::optional _val; \ diff --git a/src/cascadia/TerminalSettingsModel/Theme.h b/src/cascadia/TerminalSettingsModel/Theme.h index 96a496b908c..e39d42b8f20 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.h +++ b/src/cascadia/TerminalSettingsModel/Theme.h @@ -39,6 +39,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation 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); + uint8_t UnfocusedTabOpacity() const noexcept; WINRT_PROPERTY(til::color, Color); WINRT_PROPERTY(winrt::Microsoft::Terminal::Settings::Model::ThemeColorType, ColorType); diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 32127abf2ce..39260bb0cf4 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -32,6 +32,7 @@ namespace Microsoft.Terminal.Settings.Model Windows.UI.Xaml.Media.Brush Evaluate(Windows.UI.Xaml.ResourceDictionary res, Windows.UI.Xaml.Media.Brush terminalBackground, Boolean forTitlebar); + UInt8 UnfocusedTabOpacity { get; }; } runtimeclass WindowTheme { @@ -45,6 +46,7 @@ namespace Microsoft.Terminal.Settings.Model runtimeclass TabTheme { ThemeColor Background { get; }; + ThemeColor UnfocusedBackground { get; }; TabCloseButtonVisibility ShowCloseButton { get; }; } diff --git a/src/cascadia/WinRTUtils/inc/Utils.h b/src/cascadia/WinRTUtils/inc/Utils.h index 82824c5bfaa..ec8233fcb3c 100644 --- a/src/cascadia/WinRTUtils/inc/Utils.h +++ b/src/cascadia/WinRTUtils/inc/Utils.h @@ -53,3 +53,66 @@ winrt::Windows::Foundation::IAsyncOperation SaveFilePicker(HWND } winrt::Windows::Foundation::IAsyncOperation OpenImagePicker(HWND parentHwnd); + +#ifdef WINRT_Windows_UI_Xaml_H +// Only compile me if Windows.UI.Xaml is already included. +// +// 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. +winrt::Windows::Foundation::IInspectable ThemeLookup(const auto& res, + const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme, + const winrt::Windows::Foundation::IInspectable& 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 == winrt::Windows::UI::Xaml::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. + + const auto requestedThemeKey = requestedTheme == winrt::Windows::UI::Xaml::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); +}; +#endif diff --git a/src/inc/til/color.h b/src/inc/til/color.h index 5054be53318..ca07522b35b 100644 --- a/src/inc/til/color.h +++ b/src/inc/til/color.h @@ -134,6 +134,29 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" }; } + // source-over alpha blending/composition. + // `this` (source/top) will be blended "over" `destination` (bottom). + // `this` and `destination` are expected to be in straight alpha. + // See https://en.wikipedia.org/wiki/Alpha_compositing#Description + constexpr color layer_over(const color& destination) const + { + const auto sourceAlpha = a / 255.0f; + const auto destinationAlpha = destination.a / 255.0f; + const auto aInverse = 1.0f - sourceAlpha; + + const auto resultA = a + destination.a * aInverse; + const auto resultR = (r * sourceAlpha + destination.r * destinationAlpha * aInverse) / resultA; + const auto resultG = (g * sourceAlpha + destination.g * destinationAlpha * aInverse) / resultA; + const auto resultB = (b * sourceAlpha + destination.b * destinationAlpha * aInverse) / resultA; + + return { + static_cast(resultR + 0.5f), + static_cast(resultG + 0.5f), + static_cast(resultB + 0.5f), + static_cast(resultA + 0.5f), + }; + } + #ifdef D3DCOLORVALUE_DEFINED constexpr operator D3DCOLORVALUE() const {