Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting the window frame color #15441

Merged
merged 7 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
40 changes: 27 additions & 13 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions src/cascadia/TerminalApp/TerminalWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TerminalWindow.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/cascadia/TerminalSettingsModel/MTSMSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
3 changes: 3 additions & 0 deletions src/cascadia/TerminalSettingsModel/Theme.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
128 changes: 127 additions & 1 deletion src/cascadia/WindowsTerminal/AppHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW This is one of the things we should probably put into the renderer and not the UI parts. Over RDP it'll run too fast (<= 30Hz) and on modern PCs too slow (>= 120Hz). It's similar to our scrollbar updates, etc.

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*/,
Expand Down Expand Up @@ -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*/,
Expand Down
8 changes: 8 additions & 0 deletions src/cascadia/WindowsTerminal/AppHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class AppHost

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();
Expand Down Expand Up @@ -151,7 +154,12 @@ class AppHost
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
Expand Down
Loading