Add support for setting the window frame color (#15441)

Add support for `$theme.window.frame`, `.unfocusedFrame`, and `.rainbowFrame`. The first two accept a `ThemeColor` to set the window frame, using [`DwmSetWindowAttribute`](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmsetwindowattribute) with [`DWMWA_BORDER_COLOR`](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute). `rainbowFrame` accepts a `bool`. When enabled, it'll cycle the color of the frame through all the hues, ala [this gif](https://user-images.githubusercontent.com/18356694/164307822-e4267965-2ce0-4294-8499-59c3ba7edbae.gif) (but, constantly, instead of just when the window moves). 

This only works on Windows 11.

## Validation Steps Performed
* Works on Windows 11
* Doesn't explode on Windows 10

## PR Checklist
- [x] Closes #12950
- See also #3327
- [x] Schema updated (if necessary)


### other details

There's probably some impact to perf with `rainbowFrame`. It's one `DispatcherTimer` per window. That could probably be optimized somehow to like, one per process, but meh?
This commit is contained in:
Mike Griese 2023-06-06 18:17:03 -05:00 committed by GitHub
parent c627991522
commit 8f83322322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 190 additions and 19 deletions

View File

@ -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"
}
}
},

View File

@ -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<TerminalApp::SettingsTab>() })
{
terminalBrush = settingsTab.Content().try_as<Settings::Editor::MainPage>().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<TerminalApp::SettingsTab>())
{
return settingsTab.Content().try_as<Settings::Editor::MainPage>().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:

View File

@ -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<TerminalPage>; // for Xaml to bind events

View File

@ -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)

View File

@ -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();

View File

@ -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);

View File

@ -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) \

View File

@ -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 {

View File

@ -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<short>(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<IVirtualDesktopManager>(__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<uint8_t>(255.f * std::clamp(a, 0.f, 1.f)),
base::saturated_cast<uint8_t>(255.f * std::clamp(b, 0.f, 1.f)),
base::saturated_cast<uint8_t>(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<float> 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*/,

View File

@ -51,6 +51,9 @@ private:
std::shared_ptr<ThrottledFuncTrailing<bool>> _showHideWindowThrottler;
std::chrono::time_point<std::chrono::steady_clock> _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