diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 1900b2e4fc..9a1a24bdeb 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1813,6 +1813,19 @@ "description": "True if the Terminal should use a Mica backdrop for the window. This will apply underneath all controls (including the terminal panes and the titlebar)", "type": "boolean", "default": false + }, + "experimental.rainbowFrame": { + "description": "When enabled, the frame of the window will cycle through all the colors. Enabling this will override the `frame` and `unfocusedFrame` settings.", + "type": "boolean", + "default": false + }, + "frame": { + "description": "The color of the window frame when the window is inactive. This only works on Windows 11", + "$ref": "#/$defs/ThemeColor" + }, + "unfocusedFrame": { + "description": "The color of the window frame when the window is inactive. This only works on Windows 11", + "$ref": "#/$defs/ThemeColor" } } }, diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 24f271574d..2f6691162c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -4434,6 +4434,16 @@ namespace winrt::TerminalApp::implementation til::color bgColor = backgroundSolidBrush.Color(); + Media::Brush terminalBrush{ nullptr }; + if (const auto& control{ _GetActiveControl() }) + { + terminalBrush = control.BackgroundBrush(); + } + else if (const auto& settingsTab{ _GetFocusedTab().try_as() }) + { + terminalBrush = settingsTab.Content().try_as().BackgroundBrush(); + } + if (_settings.GlobalSettings().UseAcrylicInTabRow()) { const auto acrylicBrush = Media::AcrylicBrush(); @@ -4448,18 +4458,6 @@ namespace winrt::TerminalApp::implementation theme.TabRow().UnfocusedBackground()) : ThemeColor{ nullptr } }) { - 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); @@ -4489,11 +4487,27 @@ namespace winrt::TerminalApp::implementation 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. _SetNewTabButtonColor(bgColor, bgColor); + + // Third: the window frame. This is basically the same logic as the tab row background. + // We'll set our `FrameBrush` property, for the window to later use. + const auto windowTheme{ theme.Window() }; + if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() : + windowTheme.UnfocusedFrame()) : + ThemeColor{ nullptr } }) + { + const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) }; + FrameBrush(themeBrush); + } + else + { + // Nothing was set in the theme - fall back to null. The window will + // use that as an indication to use the default window frame. + FrameBrush(nullptr); + } } // Function Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index b137ff7295..276f6687ba 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -193,6 +193,7 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(RequestReceiveContent, Windows::Foundation::IInspectable, winrt::TerminalApp::RequestReceiveContentArgs); WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, _PropertyChangedHandlers, nullptr); + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, _PropertyChangedHandlers, nullptr); private: friend struct TerminalPageT; // for Xaml to bind events diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index c95cf78931..7b60611db9 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -944,12 +944,13 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::TitlebarBrush() { - if (_root) - { - return _root->TitlebarBrush(); - } - return { nullptr }; + return _root ? _root->TitlebarBrush() : nullptr; } + winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::FrameBrush() + { + return _root ? _root->FrameBrush() : nullptr; + } + void TerminalWindow::WindowActivated(const bool activated) { if (_root) diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index a59c139a95..a26b7c98bf 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -128,6 +128,7 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TaskbarState TaskbarState(); winrt::Windows::UI::Xaml::Media::Brush TitlebarBrush(); + winrt::Windows::UI::Xaml::Media::Brush FrameBrush(); void WindowActivated(const bool activated); bool GetMinimizeToNotificationArea(); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index d91de2dce8..2ca487be54 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -95,6 +95,7 @@ namespace TerminalApp TaskbarState TaskbarState{ get; }; Windows.UI.Xaml.Media.Brush TitlebarBrush { get; }; + Windows.UI.Xaml.Media.Brush FrameBrush { get; }; void WindowActivated(Boolean activated); String GetWindowLayoutJson(Microsoft.Terminal.Settings.Model.LaunchPosition position); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index 544366fcea..6b2a021e4c 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -132,6 +132,9 @@ Author(s): #define MTSM_THEME_WINDOW_SETTINGS(X) \ X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "applicationTheme", winrt::Windows::UI::Xaml::ElementTheme::Default) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \ + X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \ + X(bool, RainbowFrame, "experimental.rainbowFrame", false) \ X(bool, UseMica, "useMica", false) #define MTSM_THEME_TABROW_SETTINGS(X) \ diff --git a/src/cascadia/TerminalSettingsModel/Theme.idl b/src/cascadia/TerminalSettingsModel/Theme.idl index 4c2338ac60..f7f041a9dd 100644 --- a/src/cascadia/TerminalSettingsModel/Theme.idl +++ b/src/cascadia/TerminalSettingsModel/Theme.idl @@ -49,6 +49,9 @@ namespace Microsoft.Terminal.Settings.Model runtimeclass WindowTheme { Windows.UI.Xaml.ElementTheme RequestedTheme { get; }; Boolean UseMica { get; }; + Boolean RainbowFrame { get; }; + ThemeColor Frame { get; }; + ThemeColor UnfocusedFrame { get; }; } runtimeclass TabRowTheme { diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index ccc52ead0c..9d2066a5da 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -28,6 +28,8 @@ using namespace std::chrono_literals; // "If the high-order bit is 1, the key is down; otherwise, it is up." static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; +constexpr const auto FrameUpdateInterval = std::chrono::milliseconds(16); + AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, const Remoting::WindowManager& manager, @@ -38,6 +40,8 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, _peasant{ peasant }, _desktopManager{ winrt::try_create_instance(__uuidof(VirtualDesktopManager)) } { + _started = std::chrono::high_resolution_clock::now(); + _HandleCommandlineArgs(args); _HandleSessionRestore(!args.Content().empty()); @@ -419,6 +423,10 @@ void AppHost::Close() // After calling _window->Close() we should avoid creating more WinUI related actions. // I suspect WinUI wouldn't like that very much. As such unregister all event handlers first. _revokers = {}; + if (_frameTimer) + { + _frameTimer.Tick(_frameTimerToken); + } _showHideWindowThrottler.reset(); _window->Close(); @@ -1036,24 +1044,138 @@ static bool _isActuallyDarkTheme(const auto requestedTheme) } } +// DwmSetWindowAttribute(... DWMWA_BORDER_COLOR.. ) doesn't work on Windows 10, +// but it _will_ spew to the debug console. This helper just no-ops the call on +// Windows 10, so that we don't even get that spew +void _frameColorHelper(const HWND h, const COLORREF color) +{ + static const bool isWindows11 = []() { + OSVERSIONINFOEXW osver{}; + osver.dwOSVersionInfoSize = sizeof(osver); + osver.dwBuildNumber = 22000; + + DWORDLONG dwlConditionMask = 0; + VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); + + if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) + { + return true; + } + return false; + }(); + + if (isWindows11) + { + LOG_IF_FAILED(DwmSetWindowAttribute(h, DWMWA_BORDER_COLOR, &color, sizeof(color))); + } +} + void AppHost::_updateTheme() { auto theme = _appLogic.Theme(); _window->OnApplicationThemeChanged(theme.RequestedTheme()); + const auto windowTheme{ theme.Window() }; + const auto b = _windowLogic.TitlebarBrush(); const auto color = ThemeColor::ColorFromBrush(b); const auto colorOpacity = b ? color.A / 255.0 : 0.0; const auto brushOpacity = _opacityFromBrush(b); const auto opacity = std::min(colorOpacity, brushOpacity); - _window->UseMica(theme.Window() ? theme.Window().UseMica() : false, opacity); + _window->UseMica(windowTheme ? windowTheme.UseMica() : false, opacity); // This is a hack to make the window borders dark instead of light. // It must be done before WM_NCPAINT so that the borders are rendered with // the correct theme. // For more information, see GH#6620. LOG_IF_FAILED(TerminalTrySetDarkTheme(_window->GetHandle(), _isActuallyDarkTheme(theme.RequestedTheme()))); + + // Update the window frame. If `rainbowFrame:true` is enabled, then that + // will be used. Otherwise we'll try to use the `FrameBrush` set in the + // terminal window, as that will have the right color for the ThemeColor for + // this setting. If that value is null, then revert to the default frame + // color. + if (windowTheme) + { + if (windowTheme.RainbowFrame()) + { + _startFrameTimer(); + } + else if (const auto b{ _windowLogic.FrameBrush() }) + { + _stopFrameTimer(); + const auto color = ThemeColor::ColorFromBrush(b); + COLORREF ref{ til::color{ color } }; + _frameColorHelper(_window->GetHandle(), ref); + } + else + { + _stopFrameTimer(); + // DWMWA_COLOR_DEFAULT is the magic "reset to the default" value + _frameColorHelper(_window->GetHandle(), DWMWA_COLOR_DEFAULT); + } + } +} + +void AppHost::_startFrameTimer() +{ + // Instantiate the frame color timer, if we haven't already. We'll only ever + // create one instance of this. We'll set up the callback for the timers as + // _updateFrameColor, which will actually handle setting the colors. If we + // already have a timer, just start that one. + + if (_frameTimer == nullptr) + { + _frameTimer = winrt::Windows::UI::Xaml::DispatcherTimer(); + _frameTimer.Interval(FrameUpdateInterval); + _frameTimerToken = _frameTimer.Tick({ this, &AppHost::_updateFrameColor }); + } + _frameTimer.Start(); +} + +void AppHost::_stopFrameTimer() +{ + if (_frameTimer) + { + _frameTimer.Stop(); + } +} + +// Method Description: +// - Updates the color of the window frame to cycle through all the colors. This +// is called as the `_frameTimer` Tick callback, roughly 60 times per second. +void AppHost::_updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) +{ + // First, a couple helper functions: + static const auto saturateAndToColor = [](const float a, const float b, const float c) -> til::color { + return til::color{ + base::saturated_cast(255.f * std::clamp(a, 0.f, 1.f)), + base::saturated_cast(255.f * std::clamp(b, 0.f, 1.f)), + base::saturated_cast(255.f * std::clamp(c, 0.f, 1.f)) + }; + }; + + // Helper for converting a hue [0, 1) to an RGB value. + // Credit to https://www.chilliant.com/rgb2hsv.html + static const auto hueToRGB = [&](const float H) -> til::color { + float R = abs(H * 6 - 3) - 1; + float G = 2 - abs(H * 6 - 2); + float B = 2 - abs(H * 6 - 4); + return saturateAndToColor(R, G, B); + }; + + // Now, the main body of work. + // - Convert the time delta between when we were started and now, to a hue. This will cycle us through all the colors. + // - Convert that hue to an RGB value. + // - Set the frame's color to that RGB color. + const auto now = std::chrono::high_resolution_clock::now(); + const std::chrono::duration delta{ now - _started }; + const auto seconds = delta.count() / 4; // divide by four, to make the effect slower. Otherwise it flashes way to fast. + float ignored; + const auto color = hueToRGB(modf(seconds, &ignored)); + + _frameColorHelper(_window->GetHandle(), color); } void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, @@ -1203,6 +1325,10 @@ void AppHost::_PropertyChangedHandler(const winrt::Windows::Foundation::IInspect _updateTheme(); } } + else if (e.PropertyName() == L"FrameBrush") + { + _updateTheme(); + } } winrt::fire_and_forget AppHost::_WindowInitializedHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 5cd639fd5e..27b71af865 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -51,6 +51,9 @@ private: std::shared_ptr> _showHideWindowThrottler; + std::chrono::time_point _started; + winrt::Windows::UI::Xaml::DispatcherTimer _frameTimer{ nullptr }; + uint32_t _launchShowWindowCommand{ SW_NORMAL }; void _preInit(); @@ -151,7 +154,12 @@ private: void _handleSendContent(const winrt::Windows::Foundation::IInspectable& sender, winrt::Microsoft::Terminal::Remoting::RequestReceiveContentArgs args); + void _startFrameTimer(); + void _stopFrameTimer(); + void _updateFrameColor(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&); + winrt::event_token _GetWindowLayoutRequestedToken; + winrt::event_token _frameTimerToken; // Helper struct. By putting these all into one struct, we can revoke them // all at once, by assigning _revokers to a fresh Revokers instance. That'll