Skip to content

Commit

Permalink
Initial Theme support (#12992)
Browse files Browse the repository at this point in the history
##### ⚠️ 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
zadjii-msft authored Jul 7, 2022
1 parent 16028de commit 07d58a8
Show file tree
Hide file tree
Showing 46 changed files with 1,398 additions and 136 deletions.
5 changes: 5 additions & 0 deletions .github/actions/spelling/allow/apis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ DERR
dlldata
DONTADDTORECENT
DWORDLONG
DWMSBT
DWMWA
endfor
enumset
environstrings
Expand Down Expand Up @@ -95,6 +97,7 @@ lround
Lsa
lsass
LSHIFT
MAINWINDOW
memchr
memicmp
MENUCOMMAND
Expand Down Expand Up @@ -176,6 +179,8 @@ Stubless
Subheader
Subpage
syscall
SYSTEMBACKDROP
TABROW
TASKBARCREATED
TBPF
THEMECHANGED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<ClCompile Include="DeserializationTests.cpp" />
<ClCompile Include="SerializationTests.cpp" />
<ClCompile Include="TerminalSettingsTests.cpp" />
<ClCompile Include="ThemeTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
Expand Down
276 changes: 276 additions & 0 deletions src/cascadia/LocalTests_SettingsModel/ThemeTests.cpp
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;
}
}
}
40 changes: 30 additions & 10 deletions src/cascadia/TerminalApp/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static const std::array settingsLoadWarningsLabels {
USES_RESOURCE(L"InvalidSplitSize"),
USES_RESOURCE(L"FailedToParseStartupActions"),
USES_RESOURCE(L"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
};
static const std::array settingsLoadErrorsLabels {
USES_RESOURCE(L"NoProfilesText"),
Expand Down Expand Up @@ -367,11 +368,12 @@ namespace winrt::TerminalApp::implementation
// details here, but it does have the desired effect.
// It's not enough to set the theme on the dialog alone.
auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) {
auto theme{ _settings.GlobalSettings().Theme() };
auto theme{ _settings.GlobalSettings().CurrentTheme() };
auto requestedTheme{ theme.RequestedTheme() };
auto element{ sender.try_as<winrt::Windows::UI::Xaml::FrameworkElement>() };
while (element)
{
element.RequestedTheme(theme);
element.RequestedTheme(requestedTheme);
element = element.Parent().try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
}
} };
Expand Down Expand Up @@ -737,13 +739,7 @@ namespace winrt::TerminalApp::implementation

winrt::Windows::UI::Xaml::ElementTheme AppLogic::GetRequestedTheme()
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}

return _settings.GlobalSettings().Theme();
return Theme().RequestedTheme();
}

bool AppLogic::GetShowTabsInTitlebar()
Expand Down Expand Up @@ -964,7 +960,7 @@ namespace winrt::TerminalApp::implementation

void AppLogic::_RefreshThemeRoutine()
{
_ApplyTheme(_settings.GlobalSettings().Theme());
_ApplyTheme(GetRequestedTheme());
}

// Function Description:
Expand Down Expand Up @@ -1219,6 +1215,19 @@ namespace winrt::TerminalApp::implementation
return {};
}

winrt::Windows::UI::Xaml::Media::Brush AppLogic::TitlebarBrush()
{
if (_root)
{
return _root->TitlebarBrush();
}
return { nullptr };
}
void AppLogic::WindowActivated(const bool activated)
{
_root->WindowActivated(activated);
}

bool AppLogic::HasCommandlineArguments() const noexcept
{
return _hasCommandLineArguments;
Expand Down Expand Up @@ -1645,4 +1654,15 @@ namespace winrt::TerminalApp::implementation
{
return _settings.GlobalSettings().ShowTitleInTitlebar();
}

Microsoft::Terminal::Settings::Model::Theme AppLogic::Theme()
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}
return _settings.GlobalSettings().CurrentTheme();
}

}
Loading

0 comments on commit 07d58a8

Please sign in to comment.