-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#####⚠️ targeting 1.15 ## Summary of the Pull Request Adds support for Themes, a new type of customization for the Terminal. Themes allow the user to customize elements of the Terminal window itself. In this first iteration, this PR adds support for two main properties: * enabling Mica as the window backdrop * changing the tab row color (read: changing the titelbar color) These represent the most important asks of theming in the Terminal. The properties added in this PR are: * Theme color variants: - `"#rrggbb"` or `"#aarrggbb"` - `"accent"` - `"terminalBackground"` * Properties (_listed here in dot notation, but implemented as sub-objects_) - `tabRow.background`: accepts a ThemeColor (above) - `window.applicationTheme`: accepts one of `{"system", "light", "dark"}` - `window.useMica`: accepts a boolean, defaults to false. ## References * As first described in #3327 * spec'd in #12530 ## PR Checklist * [x] Sorta enables #10509, but doesn't close it. That'll need more comprehensive changes to the titlebar code. * **update**: I totally disabled mica, but left the serialization code. It just seems silly without #10509. * [x] Closes #1963 * [x] Closes #3774 * [x] Closes #12939 * [x] Does the bulk of the #3327 work, but I'm going to leave that open since that's become my megathread for everything related to theming. * [x] I work here * [x] Tests added/passed * [ ] Requires documentation to be updated - **SURE DOES** ## Detailed Description of the Pull Request / Additional comments ### --> GO READ #12530 <-- Seriously. These themes aren't customizable in the SUI currently. You can change the active theme, and the UI will show all of the defined themes, but they're not editable there. They don't layer. You'll need to define your own themes. Thay can't come from fragments. This is a really cool future idea, but not implemented in this v0. The sub objects have some gnarly macros to generate a lot of the serialization code for you. ### TODOs * [x] I still have yet to establish what the accent color algorithm is. This might be proprietary and require a ThemeHelpers workaround. * [x] Make sure `terminalBackground` & the SUI result in something sensible * [x] Make sure runtime BG changes work with `terminalBackground`. One time, they didn't. `printf "\x1b]11;rgb:ff/00/ff\x07"` * [x] Acrylic Terminal BG's look weird, like, the opacity is always 50% or something. And the tab row looks all wrong then. ## Validation Steps Performed This is the blob I've been testing with: <details> ```jsonc // "useAcrylicInTabRow": true, "theme": "my dark", // "theme": "Edge", "theme": "orangey", "theme": "WHITE", // "theme": "terminal", "themes": [ { "name": "my dark", "window": { "applicationTheme": "dark", "useMica": true, }, "tabRow": { "background": "#00000000", }, }, { "name": "Edge", "tabRow": { "background": "accent" }, "window": { "applicationTheme": "system" } }, { "name": "orangey", "window": { "applicationTheme": "light", "useMica": true, }, "tabRow": { "background": "#ff8800", }, }, { "name": "WHITE", "window": { "applicationTheme": "dark", "useMica": true, }, "tabRow": { "background": "#FFFFFF", }, }, { "name": "terminal", "window": { "applicationTheme": "dark", "useMica": false, }, "tabRow": { "background": "terminalBackground", }, }, ] ``` </details>
- Loading branch information
1 parent
16028de
commit 07d58a8
Showing
46 changed files
with
1,398 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "pch.h" | ||
|
||
#include "../TerminalSettingsModel/Theme.h" | ||
#include "../TerminalSettingsModel/CascadiaSettings.h" | ||
#include "../types/inc/colorTable.hpp" | ||
#include "JsonTestClass.h" | ||
|
||
#include <defaults.h> | ||
|
||
using namespace Microsoft::Console; | ||
using namespace winrt::Microsoft::Terminal; | ||
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation; | ||
using namespace WEX::Logging; | ||
using namespace WEX::TestExecution; | ||
using namespace WEX::Common; | ||
|
||
namespace SettingsModelLocalTests | ||
{ | ||
// TODO:microsoft/terminal#3838: | ||
// Unfortunately, these tests _WILL NOT_ work in our CI. We're waiting for | ||
// an updated TAEF that will let us install framework packages when the test | ||
// package is deployed. Until then, these tests won't deploy in CI. | ||
|
||
class ThemeTests : public JsonTestClass | ||
{ | ||
// Use a custom AppxManifest to ensure that we can activate winrt types | ||
// from our test. This property will tell taef to manually use this as | ||
// the AppxManifest for this test class. | ||
// This does not yet work for anything XAML-y. See TabTests.cpp for more | ||
// details on that. | ||
BEGIN_TEST_CLASS(ThemeTests) | ||
TEST_CLASS_PROPERTY(L"RunAs", L"UAP") | ||
TEST_CLASS_PROPERTY(L"UAP:AppXManifest", L"TestHostAppXManifest.xml") | ||
END_TEST_CLASS() | ||
|
||
TEST_METHOD(ParseSimpleTheme); | ||
TEST_METHOD(ParseEmptyTheme); | ||
TEST_METHOD(ParseNoWindowTheme); | ||
TEST_METHOD(ParseNullWindowTheme); | ||
TEST_METHOD(ParseThemeWithNullThemeColor); | ||
TEST_METHOD(InvalidCurrentTheme); | ||
|
||
static Core::Color rgb(uint8_t r, uint8_t g, uint8_t b) noexcept | ||
{ | ||
return Core::Color{ r, g, b, 255 }; | ||
} | ||
}; | ||
|
||
void ThemeTests::ParseSimpleTheme() | ||
{ | ||
static constexpr std::string_view orangeTheme{ R"({ | ||
"name": "orange", | ||
"tabRow": | ||
{ | ||
"background": "#FFFF8800", | ||
"unfocusedBackground": "#FF884400" | ||
}, | ||
"window": | ||
{ | ||
"applicationTheme": "light", | ||
"useMica": true | ||
} | ||
})" }; | ||
|
||
const auto schemeObject = VerifyParseSucceeded(orangeTheme); | ||
auto theme = Theme::FromJson(schemeObject); | ||
VERIFY_ARE_EQUAL(L"orange", theme->Name()); | ||
|
||
VERIFY_IS_NOT_NULL(theme->TabRow()); | ||
VERIFY_IS_NOT_NULL(theme->TabRow().Background()); | ||
VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); | ||
VERIFY_ARE_EQUAL(rgb(0xff, 0x88, 0x00), theme->TabRow().Background().Color()); | ||
|
||
VERIFY_IS_NOT_NULL(theme->Window()); | ||
VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Light, theme->Window().RequestedTheme()); | ||
VERIFY_ARE_EQUAL(true, theme->Window().UseMica()); | ||
} | ||
|
||
void ThemeTests::ParseEmptyTheme() | ||
{ | ||
Log::Comment(L"This theme doesn't have any elements defined."); | ||
static constexpr std::string_view emptyTheme{ R"({ | ||
"name": "empty" | ||
})" }; | ||
|
||
const auto schemeObject = VerifyParseSucceeded(emptyTheme); | ||
auto theme = Theme::FromJson(schemeObject); | ||
VERIFY_ARE_EQUAL(L"empty", theme->Name()); | ||
VERIFY_IS_NULL(theme->TabRow()); | ||
VERIFY_IS_NULL(theme->Window()); | ||
VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); | ||
} | ||
|
||
void ThemeTests::ParseNoWindowTheme() | ||
{ | ||
Log::Comment(L"This theme doesn't have a window defined."); | ||
static constexpr std::string_view emptyTheme{ R"({ | ||
"name": "noWindow", | ||
"tabRow": | ||
{ | ||
"background": "#FF112233", | ||
"unfocusedBackground": "#FF884400" | ||
}, | ||
})" }; | ||
|
||
const auto schemeObject = VerifyParseSucceeded(emptyTheme); | ||
auto theme = Theme::FromJson(schemeObject); | ||
VERIFY_ARE_EQUAL(L"noWindow", theme->Name()); | ||
|
||
VERIFY_IS_NOT_NULL(theme->TabRow()); | ||
VERIFY_IS_NOT_NULL(theme->TabRow().Background()); | ||
VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); | ||
VERIFY_ARE_EQUAL(rgb(0x11, 0x22, 0x33), theme->TabRow().Background().Color()); | ||
|
||
VERIFY_IS_NULL(theme->Window()); | ||
VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); | ||
} | ||
|
||
void ThemeTests::ParseNullWindowTheme() | ||
{ | ||
Log::Comment(L"This theme doesn't have a window defined."); | ||
static constexpr std::string_view emptyTheme{ R"({ | ||
"name": "nullWindow", | ||
"tabRow": | ||
{ | ||
"background": "#FF112233", | ||
"unfocusedBackground": "#FF884400" | ||
}, | ||
"window": null | ||
})" }; | ||
|
||
const auto schemeObject = VerifyParseSucceeded(emptyTheme); | ||
auto theme = Theme::FromJson(schemeObject); | ||
VERIFY_ARE_EQUAL(L"nullWindow", theme->Name()); | ||
|
||
VERIFY_IS_NOT_NULL(theme->TabRow()); | ||
VERIFY_IS_NOT_NULL(theme->TabRow().Background()); | ||
VERIFY_ARE_EQUAL(Settings::Model::ThemeColorType::Color, theme->TabRow().Background().ColorType()); | ||
VERIFY_ARE_EQUAL(rgb(0x11, 0x22, 0x33), theme->TabRow().Background().Color()); | ||
|
||
VERIFY_IS_NULL(theme->Window()); | ||
VERIFY_ARE_EQUAL(winrt::Windows::UI::Xaml::ElementTheme::Default, theme->RequestedTheme()); | ||
} | ||
|
||
void ThemeTests::ParseThemeWithNullThemeColor() | ||
{ | ||
Log::Comment(L"These themes are all missing a tabRow background. Make sure we don't somehow default-construct one for them"); | ||
|
||
static constexpr std::string_view settingsString{ R"json({ | ||
"themes": [ | ||
{ | ||
"name": "backgroundEmpty", | ||
"tabRow": | ||
{ | ||
}, | ||
"window": | ||
{ | ||
"applicationTheme": "light", | ||
"useMica": true | ||
} | ||
}, | ||
{ | ||
"name": "backgroundNull", | ||
"tabRow": | ||
{ | ||
"background": null | ||
}, | ||
"window": | ||
{ | ||
"applicationTheme": "light", | ||
"useMica": true | ||
} | ||
}, | ||
{ | ||
"name": "backgroundOmittedEntirely", | ||
"window": | ||
{ | ||
"applicationTheme": "light", | ||
"useMica": true | ||
} | ||
} | ||
] | ||
})json" }; | ||
|
||
try | ||
{ | ||
const auto settings{ winrt::make_self<CascadiaSettings>(settingsString, DefaultJson) }; | ||
|
||
const auto& themes{ settings->GlobalSettings().Themes() }; | ||
{ | ||
const auto& backgroundEmpty{ themes.Lookup(L"backgroundEmpty") }; | ||
VERIFY_ARE_EQUAL(L"backgroundEmpty", backgroundEmpty.Name()); | ||
VERIFY_IS_NOT_NULL(backgroundEmpty.TabRow()); | ||
VERIFY_IS_NULL(backgroundEmpty.TabRow().Background()); | ||
} | ||
{ | ||
const auto& backgroundNull{ themes.Lookup(L"backgroundNull") }; | ||
VERIFY_ARE_EQUAL(L"backgroundNull", backgroundNull.Name()); | ||
VERIFY_IS_NOT_NULL(backgroundNull.TabRow()); | ||
VERIFY_IS_NULL(backgroundNull.TabRow().Background()); | ||
} | ||
{ | ||
const auto& backgroundOmittedEntirely{ themes.Lookup(L"backgroundOmittedEntirely") }; | ||
VERIFY_ARE_EQUAL(L"backgroundOmittedEntirely", backgroundOmittedEntirely.Name()); | ||
VERIFY_IS_NULL(backgroundOmittedEntirely.TabRow()); | ||
} | ||
} | ||
catch (const SettingsException& ex) | ||
{ | ||
auto loadError = ex.Error(); | ||
loadError; | ||
throw ex; | ||
} | ||
catch (const SettingsTypedDeserializationException& e) | ||
{ | ||
auto deserializationErrorMessage = til::u8u16(e.what()); | ||
Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); | ||
throw e; | ||
} | ||
} | ||
|
||
void ThemeTests::InvalidCurrentTheme() | ||
{ | ||
Log::Comment(L"Make sure specifying an invalid theme falls back to a sensible default."); | ||
|
||
static constexpr std::string_view settingsString{ R"json({ | ||
"theme": "foo", | ||
"themes": [ | ||
{ | ||
"name": "bar", | ||
"tabRow": {}, | ||
"window": | ||
{ | ||
"applicationTheme": "light", | ||
"useMica": true | ||
} | ||
} | ||
] | ||
})json" }; | ||
|
||
try | ||
{ | ||
const auto settings{ winrt::make_self<CascadiaSettings>(settingsString, DefaultJson) }; | ||
|
||
VERIFY_ARE_EQUAL(1u, settings->Warnings().Size()); | ||
VERIFY_ARE_EQUAL(Settings::Model::SettingsLoadWarnings::UnknownTheme, settings->Warnings().GetAt(0)); | ||
|
||
const auto& themes{ settings->GlobalSettings().Themes() }; | ||
{ | ||
const auto& bar{ themes.Lookup(L"bar") }; | ||
VERIFY_ARE_EQUAL(L"bar", bar.Name()); | ||
VERIFY_IS_NOT_NULL(bar.TabRow()); | ||
VERIFY_IS_NULL(bar.TabRow().Background()); | ||
} | ||
|
||
const auto currentTheme{ settings->GlobalSettings().CurrentTheme() }; | ||
VERIFY_IS_NOT_NULL(currentTheme); | ||
VERIFY_ARE_EQUAL(L"system", currentTheme.Name()); | ||
} | ||
catch (const SettingsException& ex) | ||
{ | ||
auto loadError = ex.Error(); | ||
loadError; | ||
throw ex; | ||
} | ||
catch (const SettingsTypedDeserializationException& e) | ||
{ | ||
auto deserializationErrorMessage = til::u8u16(e.what()); | ||
Log::Comment(NoThrowString().Format(deserializationErrorMessage.c_str())); | ||
throw e; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.