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:
parent
c627991522
commit
8f83322322
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) \
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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*/,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue