33 KiB
author | created on | last updated | issue id |
---|---|---|---|
Mike Griese @zadjii-msft | 2019-12-13 | 2022-04-25 |
TODO!S:
- How do themes play with different window title settings? (different themes for different windows.
_quake
esp.)) - any clever ideas for elevated themes?
- Reconcile with global
experimental.useBackgroundImageForWindow
from #12893
Application Theming
Abstract
This spec outlines how the Windows Terminal will enable users to create custom "themes" for the application, enabling further customization of the window. These themes will be implemented as objects containing a group of UI-specific properties, so users can quickly apply a group of properties atomically.
Inspiration
Much of the inspiration for this feature comes from VsCode and its themes. These themes can be more than just different color palettes for the editor - these themes can control the appearance of a variety of UI elements of the VsCode window.
Solution Design
Requested Functionality ("User Stories")
The following is a long list of ideas of elements of the window that the user should be able to customize:
- Pane Border colors (both the background, and the "focused" color) (#3061)
- Pane border width (#3062)
- Tab Row and Item Background color (#702/#1337/#2994/#3774/#1963)
- Some users want to set these to the accent color
- Some users want to set these to a specific custom color
- Some users want this to use the color straight from the active Terminal, allowing the tab or titlebar to "blend into" the terminal
- Feature Request: Setting to hide/remove close ("x") button from tabs (#3335)
- Various different tab sizing modes
- the current sizing, which is
SizeToContent
- Setting a min/max width on tabs
- Configuring tabs to split the available space
- the current sizing, which is
Other lower-priority ideas:
- Enable hiding the tab icon altogether
- Enable forcing tab icons to monochrome
- Tab row height
- Tab row font size, font face
- Tab corner radius
- Margin between tabs? Padding within the tab?
- Left-justify / Center / right-justify tab text, when tabs are wider than their text?
- Control colors for light vs dark vs high-contrast modes
- Enable/disable a shadow underneath the tab row, between tabs and content
- Enable/disable a shadow cast by terminals on pane borders or a shadow cast by pane borders on Terminal panes
- Similarly to the tabs, styling the Status Bar (#3459)
- Maybe enable it to have the same color as the active TermControl, causing the same "seamless" effect (see this comment)
- Change font size, face, colors
- Control the borders on the status bar - no top border would give the impression it's "seamless"
Additionally, the user should be able to easily switch from one installed theme to another. The user should be able to copy a simple blob of settings from the web and paste it into their settings to be able to easily try the theme out.
Difference between "Themes" and "Schemes"
The avid follower of the Windows Terminal might know that the Terminal already contains support for "color schemes". What makes themes different from these schemes, and why should they be separate objects?
Color Schemes are objects that generally control the appearance of the Terminal Control itself (the proverbial "black rectangle with text in it"). Primarily, color schemes are used for setting the "color table" of a terminal instance, setting the values for each of the 16 colors in the terminal's color table, and the default foreground and background colors. These are properties that only apply to the contents of the terminal itself, and not necessarily the entire application. Individual terminal control instances can have different color schemes. Furthermore, these schemes are largely in-line with schemes available on other platform's terminals. These schemes were heavily inspired by the great work done at iTerm2-Color-Schemes.
Alternatively, Themes are sets of properties that apply primarily to the window of the application itself, but not necessarily the terminal content. These properties apply globally to the entire window, as opposed to controlling the appearance of individual terminals. These properties include things such as the coloration and styling of the tabs in the tab row.
Theme objects
Themes will be implemented largely similar to the color schemes implementation.
Currently, the terminal contains a list of available color schemes, and profiles
can chose to apply a scheme from the list of schemes. We'll add a list of
themes
, and globally, the user will be able to specify one of these themes to
apply.
Take for example the following settings excerpt:
{
"theme": "My Boxy Theme",
"themes": [
{
"name": "My Boxy Theme",
"window":{
"applicationTheme": "dark"
},
"tab": {
"radius": 0,
"padding": 5,
"background": "terminalBackground",
"textColor": "key:SystemAccentColorLight3",
"icon": "outline",
"closeButton": "hidden",
},
"tabRow":{
"background": "accent",
"shadows": false
}
},
{
"name": "My small light theme",
"window":{
"applicationTheme": "light"
},
"tab": {
"background": "#80ff0000",
"height": 8,
"icon": "hidden",
"closeButton": "hover"
},
"tabRow":{
"background": "#ffffffff",
"acrylicOpacity": 50,
}
}
]
}
In the above settings snippet, we see the following things:
- A list of
themes
that the user can pick from. Each theme has aname
property used to identify the theme, and a group of properties for the theme. - The user has set the
theme
to"My Boxy Theme"
, the first theme in the list of themes. If the user wanted to switch to the other installed theme,"My small light theme"
, they'd simply need to change this property.
note: Initially, we had considered a
elementPropertyName
-like syntax as opposed to the object-grouped one above. We also considered aelement.propertyName
-like syntax. Overall, we liked the object based one best.For simplicity, we'll be using
element.propertyName
syntax throughout to refer to these properties, when grouped underelement
objects in the theme.
These Theme objects are designed to make it simple for the user to be able to quickly download these as an extension in the future, and hot-switch between them. Imagine: an application that would provide a gallery of uploaded themes, and the user could install them as fragment extensions.
Exposed theme properties
Themes should be able to control a variety of elements of the Terminal UI. Some of these settings will be easier to implement than others. As such, below is a set of properties that seems appropriate to include as part of a "v1" theming implementation. In Future Considerations, we'll enumerate additional properties that could be added in the future to further control the UI.
Highest priority theming properties
These are the elements that have orders of magnitude more requests:
- Customizing the titlebar color, including the unfocused titlebar color. This
includes merging with the existing
useAcrylicInTabRow
setting. - Customizing the tab color
- Enabling Mica for the window
These represent the most important asks of theming in the Terminal. Everything else that follows is merely "nice to have". The most important elements then are:
- Properties:
tab.background
tabRow.background
tabRow.acrylicOpacity
tabRow.unfocusedBackground
window.background.useMica
- Theme color variants:
"#rrggbb"
or"#rrggbbaa"
"accent"
"terminalBackground"
Additional theming v1 Properties
These are additional settings that seem of higher priority or would be easier to implement. They are categorized by the element of the Terminal they are controlling:
Individual Tabs
tab.cornerRadius
: Control the radius of the corners of the tab items. Accepts adouble
. If this is set to0
, then the tabs will have squared-off corners. No particular limit is set on the max value accepted, though larger values might not be aesthetically pleasing.tab.bottomCornerRadius
: Control the radius of the bottom corners of the tab items. This can be used to make the tabs look like "buttons" in the tab row, instead of tabs.tab.closeButton
: Control the visibility of the close button for a tab item. Accepts the following values:visible
: The default behavior of the tab item close button - always visible.hover
: The close button on a tab item only appears when the tab is hovered.hidden
: The close button on a tab is always hidden.
tab.icon
: Control the visibility, appearance of the tab iconvisible
: The default behavior of the tab item icon - always visible, and in full color.outline
: The icon is always visible, but is only drawn as an outline, usingBitmapIconSource.ShowAsMonochrome(true)
hidden
: The icon is hidden
tab.background
: Control the color of the background of tab items. See below for accepted colors.
Tab Row / "Titlebar"
tabRow.background
: Control the color of the background of the tab row. When tabs in the titlebar are enabled, this sets the color of the titlebar. See below for accepted colors.- Notably, this is named
tabRow.background
, nottitlebar.background
. Outside of tabs-in-titlebar mode, we can't control the window titlebar color. - This ignores any alpha and always uses 1.0 for the alpha channel. See Titlebar complications for details.
- Notably, this is named
tabRow.unfocusedBackground
: Control the color of the background of the tab row, when the window is unfocused. See below for accepted colors.- TODO! When omitted, should this default to the
tabRow.background
value (if set), or just the normal unfocused window color? "the normal unfocused window color" is a SUBSTANTIALLY easier implementation. - This ignores any alpha and always uses 1.0 for the alpha channel. See Titlebar complications for details.
- TODO! When omitted, should this default to the
tabRow.acrylicOpacity
: Optional integer representation of an opacity (0-100). When provided, thetabRow.background
color is treated as an acrylic brush, with the givenTintOpacity
. When omitted,tabRow.background
is treated as a solid color.- This is to replace the original
useAcrylicInTabRow
setting. - This is NOT provided for the
tabRow.unfocusedBackground
setting. See Titlebar complications for details.
- This is to replace the original
Panes
pane.borderColor
: Control the color of the border used to separate panes. This is the color of the inactive border between panes.pane.activeBorderColor
: Control the color of the border of the active panepane.borderWidth
: Control the width of the borders used to separate panes.
Window Properties
window.applicationTheme
: If set, will set the XAMLRequestedTheme
property. This can be one oflight
,dark
orsystem
. This controls how XAML fundamentally styles UI elements. If not provided, will use the default value "system", which will use whatever the system's default theme is.window.roundedCorners
: A boolean, to control whether the window has rounded corners on Windows 11.window.background.useMica
: a boolean that enables/disables Mica. For more discussion, see Mica Spec.window.background.image
: a path to an image to use as the background for the whole of the content of the Terminal, including in the tab row space.- Additional properties to control the sizing of this image (
padding
,stretchMode
,opacity
, etc) would also be exposed aswindow.background.imagePadding
, a la the similar Profile settings.
- Additional properties to control the sizing of this image (
Theme Colors
For properties like tab.background
and tabRow.background
, these colors can
be one of:
- an
#rrggbb
,#aarrggbb
color. (Alpha is ignored fortabRow.background
) accent
for the titlebar version of the accent color. Notably, this is not just someSystemAccentColor
value, it's apparently some other value. This has a different value depending on if the window is focused or not. Refer to Edge the "use accent color on titlebars" setting enabled as a reference.terminalBackground
to use the default background color of the active terminal instance.terminalForeground
to use the default foreground color of the active terminal instance.key:SomeXamlKey
to try and lookSomeXamlKey
up from our resources as aColor
, and use that color for the value.accent
is NOT the same thing askey:SystemAccentColor
? If it is, is it a reasonable alias that we'd want to provide anyways?- TODO! DISCUSSION: PR#5280 suggested
{ "key": "SomeResourceKey" }
for string resources, should we use that format for colors like this as well?
This will enable users to not only provide custom colors, but also use the dynamic color of the active terminal instance as well.
Using terminalBackground
with multiple concurrent panes with different
backgrounds could certainly lead to some odd behavior. The intention of the
setting is to provide a way for the tab/titlebar to "seamlessly" connect to the
terminal content. However, two panes side-by-side could have different
background colors, which might have an unexpected appearance. Since the user
must have opted in to this behavior, they'll need to decide personally if
that's something that bothers them aesthetically. It's entirely possible that a
user doesn't use panes, and this wouldn't even be a problem for them.
Implementation of theming
Largely, whenever possible, we should be able to implement this theming support
by modifying our application's ResourceDictionary
with custom values to
control the appearance of UI elements.
For example, the TabView
already exposes a number of XAML resources we can
modify to adjust it's appearance.
TabViewBackground
controls the appearance of the background of the tab view. InshowTabsInTitlebar: true
mode, this is the color of the titlebar.TabViewItemHeaderBackground
andTabViewItemHeaderBackgroundSelected
control the appearance of an individual tab.
By modifying the values of these brushes, we can control the appearance of the
tabs. So long as we only in-place modify the resources, XAML is smart enough to
be able to update it's appearance automatically. We can do this by querying the
ResourceDictionary
for a given resource, and changing it's value, rather than
insert
ing a new value into the ResourceDictionary
to replace the old one.
In addition to the above properties, I propose adding a couple of our own
properties: PaneBorderWidth
: To control the width of pane borders
PaneBorderBrush
: To control the appearance of inactive pane borders
ActivePaneBorderBrush
: To control the appearance of active pane borders
In order to respond to the live-updating of the TermControl
's background
color, we'll need to add some more specific logic beyond simply updating a XAML
resource when settings change. Whenever a TermControl
's background color
changes, or the active pane in a tab changes:
- If
tab.background == "terminalBackground"
:- If this control is the tab's active terminal control (and the tab doesn't
have a custom color set by the color picker), update the tab's own
TabViewItem
with updatedTabViewItemHeaderBackground
andTabViewItemHeaderBackgroundSelected
values.- Here, we don't want to update the
App
's resources, since those apply globally, and each tab might have a control with a different color. - The color set by the color picker should override the color from the theme (as the former is a run-time property set to override the latter).
- Here, we don't want to update the
- If this control is the tab's active terminal control (and the tab doesn't
have a custom color set by the color picker), update the tab's own
- If
tabRow.background == "terminalBackground"
:- If this control is the active terminal of the active
Tab
, then we need to raise an event to communicate this updated value up to the window layer. We'll raise a"TabRowBackgroundBrush"
property changed event, that the app host can listen for and use to set the titlebar's color, if needed. - The
TerminalPage
also will need to set the Background of theTabRowControl
to match.
- If this control is the active terminal of the active
The tab.cornerRadius
might be a bit trickier to implement. Currently, there's
no XAML resource that controls this, nor is this something that's exposed by
the TabView control. Fortunately, this is something that's exposed to us
programmatically. We'll need to manually set that value on each TabViewItem
as
we create new tabs. When we reload settings, we'll need to make sure to come
through and update those values manually.
NOTE: microsoft-ui-xaml#2201 suggested that this will be possible with a future MUX version and changing the
OverlayCornerRadius
.
Tab Background Color, Overline Color, and the Tab Color Picker
Concurrently with the writing of this spec, work is being done to add a "color picker" for tabs, that lets the user manually set a background color for tabs. This may in the future cause some complications with setting a color for tabs from the theme.
When both features are combined, the color set at runtime but the color picker should override whatever color the user has set in the theme. When the color picker "clears" the color it has set for the tab, it should revert to the color from the theme (if one is set).
Also mentioned in the implementation of the color picker feature was the ability to not set the entire color of the tab, but just the color of a tab "overline", similar to the way Firefox (by default) styles the focused tab.
Currently, the TabView
doesn't support a tab "overline" like this, however, in
the future where this is possible, we'd love to also support such an overline.
However, the story of setting the tab color manually becomes a bit more
confusing now.
- The user should be able to set both the
tab.background
andtab.overline
colors in a theme. - The user should be able to configure whether the color picker sets the
background
or theoverline
color of the tab.
The second setting added above will allow the user to change what's controlled by the color picker. Similarly to how the color picker can set the background of the tab to override the background from the theme, the user could configure the color picker to be able to change the overline color, not the background color of the tab. Then, when the user uses the color picker, the overline color will be overridden by the color picker, instead of the tab background color.
Other things to consider:
- Users might want to be able to set a tab color as a part of the profile. One
could imagine wanting to set the tab background color for Windows PowerShell
to
rgb(1, 36, 86)
automatically. If we make this property part of the Profile, then we should use the profile's value as the runtime-override (of the theme value) for this property. If the color picker is used to set the color of the tab, then it'll override the runtime color for that tab.- How does this interact with multiple Panes in a tab? Should the Tab
override color be per-terminal instance? If the terminal has a tab color,
then that would override the theme, but not the tab's own override color?
- If that were the case, the order of precedence would be:
- A color set at runtime with the color picker
- A color from the active terminal within the tab, if it has one
- The tab color from the theme
- If that were the case, the order of precedence would be:
- How does this interact with multiple Panes in a tab? Should the Tab
override color be per-terminal instance? If the terminal has a tab color,
then that would override the theme, but not the tab's own override color?
- Users might want to be able to configure the titlebar to use a color based off
the active tab color. We might need an additional special value like
terminalBackground
that lets users say "I want to use the active tab color as the titlebar color".- During #3789, there was a point where the terminal raised actually implemented something like this. In it's implementation, the titlebar color would be slightly lighter or darker than the tab color (to provide some contrast). We'd want to make sure that the user could specify both "I want to use the tab color with some contrast applied" or just literally "Use whatever the active tab's color is."
Default Themes
Late in 1.0, we renamed the old property requestedTheme
to just theme
.
Currently, the user can use that property to simply set the XAML
RequestedTheme
property, which controls the theming of all the XAML UI
elements. Currently, they can set that value to one of light
, dark
or
system
.
To maintain backwards compatibility with that setting, we'll introduce three
themes to defaults.json
:
"themes": [
{
"name": "light",
"window":{
"applicationTheme": "light"
},
},
{
"name": "dark",
"window":{
"applicationTheme": "dark"
},
},
{
"name": "system",
"window":{
"applicationTheme": "system"
},
}
]
Each of these themes will only define one property by default: the
window.applicationTheme
property, which is now responsible for setting the
XAML RequestedTheme
property. With these default themes, the user will still
be able to use the old names seamlessly to get the same behavior.
Additionally, the user will NOT be able to override these built-in themes. Experience trying to not serialize the default color schemes has proven exceptionally tricky, so we're not going to allow that for the built-in themes. The user will always need to fork them to create a new theme. If they're found in the user settings file, we'll just ignore them.
UI/UX Design
[TODO!]: # TODO: We should include more mockups here. That would be nice.
fig 1: Using a tab color set to "terminalBackground". The Windows PowerShell
tab has also set its color with the color picker.
fig 2: Using an acrylic titlebar color, with a tab color set to
"terminalBackground"
fig 3: Using an acrylic terminal background, and the titlebar color is set to
"terminalBackground"
fig
4: Using a single image as the background for the window, with a transparent
tab row, and rounded bottoms on the TabViewItems. Courtesy of
@Shomnipotence
fig
5: Using a bottom corner radius to make tabs appear like buttons on the tab row. Courtesy of
@simioni
[TODO!]: # TODO: Settings UI mocks? These pretty substantially affect the UI.
Potential Issues
It's totally possible for the user to set some sort of theme that just looks bad. This is absolutely a "beauty in the eye of the beholder" situation - not everyone is going to like the appearance of every theme. The goal of the Terminal is to provide a basic theme that's appropriate for anyone, but empower users to customize the terminal however they see fit. If the user chooses a theme that's not particularly appealing, they can always change it back.
Accessibility
For people using the default theming, there should not be any particular regressions. However, this change does open up the Terminal to changes that might make the Terminal less accessible with certain theme configurations. As these themes would all be user-defined and controlled by the user, we're not concerned that this will be much of an issue. If a user finds one of their themes is less accessible, they can always change the theme to be more appropriate for them, or even switch to another theme.
Furthermore, this might help certain accessibility stories. Users could pick themes with even more contrast than the Terminal provides by default, or larger font sizes, which might help make parts of the Terminal more visible than the default UI.
Security
This should not introduce any new security concerns. We're relying on the security of jsoncpp for parsing json. Adding new keys to the settings file will rely on jsoncpp's ability to securely parse those json values.
Reliability
This change should not have any particular reliability concerns.
Compatibility
The biggest compatibility concern is regarding the existing values for the
theme
property, which is addressed above.
useAcrylicInTabRow
migration
[TODO!]: # TODO: Deprecating the current titlebar acrylic setting, or totally overriding in theme.
experimental.useBackgroundImageForWindow
migration
[TODO!]: # TODO: Deprecating the current setting or migrating or whatever
Performance, Power, and Efficiency
This change should not have any particular performance concerns. Additional acrylic usage might impact battery life. There's not much concern for any substantial new impacts, however.
Branding
Are we concerned that by enabling theming, the appearance of the Terminal won't be as static, and won't necessarily have as specific a look? It might be harder for potential users see a screenshot of the Terminal and know "Thats the Windows Terminal". Is this something we're really all that concerned about though? If this is something users want (it is), then shouldn't that be what matters?
Titlebar complications
Unfortunately, the original User32 titlebar is actually always drawn underneath our titlebar. Even when the tabs are in the titlebar, that's actually just XAML content drawn on top of the original frame. The rest of the window is transparent, but the titlebar is there.
Our design to enable unfocused acrylic to work relies on in-app acrylic to allow the acrylic to blur with the transparent window contents. However, since the User32 titlebar is always there, in-app acrylic would end up always blurring the original titlebar, which looks ridiculous. This means we can't have unfocused acrylic without showing that titlebar. We'd rather remove that foot gun, and make it explicit that this setting does not exist.
Light & dark mode theming
One request that comes up with frequency is the ability to change the color scheme of a profile automatically based on the system theme. Many users have scripts that automatically change between light and dark theme in the OS based on time of day.
One thing this design does not do well is account for such theme-switching
scenarios. This design assumes a static set of colors for a whole Terminal
theme, regardless of whatever window.applicationTheme
is set to. Should the
user leave window.applicationTheme
set to system
, it's entirely likely that
they would like the rest of their colors to automatically update to match.
To address this, we'll allow the window-level theme
property to not only allow
a string for a name-based lookup in the list of themes, but als an object. That
object will accept two properties: light
and dark
. Each of these accepts a
string representing the name of a theme to use for that specific OS theme. These
strings will default to "light"
and "dark"
respectively.
{
"theme": {
"light": "my light theme",
"dark": "my dark theme"
}
}
Admin window themes
[TODO!]: # TODO! Any clever ideas?
Same idea as the light vs dark mode theme ideas. How should users be able to style admin vs regular windows?
Addenda
This spec also has a follow-up spec which elaborates on the complexities of Mica in the Terminal. Please also refer to:
Future considerations
- Mentioned in #7005 was the idea of shipping a default theme that had values
aligned with the appearance of the Edge browser. Perhaps something like:
{ "name": "Edge", "window":{ "applicationTheme": "system" }, "tab": { "background": "#whatever-color-edge-is" // Might need a "key:" resource here for light/dark theme switching }, "tabRow":{ "background": "accent", } },
- Applications should be able to install themes as fragments.
- We probably shouldn't allow layering for fragment themes - don't want
foo.exe
installing alight
theme that totally overrides the built-in one. Right? TODO! DISCUSSION
- We probably shouldn't allow layering for fragment themes - don't want
- ~I don't think it's unreasonable to implement support for
theme
as either a string or an object. Iftheme
is a string, then we can do a name-based lookup in a table of themes. If it's an object, we can just use that object immediately. Doing this might provide a simpler implementation plan whereby we allow"default"|"light"|"dark"|{object}
at first, and then later add the list of themes.~- This was a cool idea, but ultimately discarded in favor of the OS light/dark
theme switching, which needed the object version of
theme
to be reserved for the OS mode lookup.
- This was a cool idea, but ultimately discarded in favor of the OS light/dark
theme switching, which needed the object version of
- A cool idea from discussion:
window.highContrastSchemes
as a theme member that controls a per-control property. This would override the color scheme of any pane with a high contrast version, ignoring any colors emitted by the client application. Details are left for a future spec.
Theming v2 Properties
tab.padding
: Control the padding within a tab between the text and the "sides" of the tabtab.textColor
: Change the color of the text on a tabtabRow.shadows
: Enable/disable the tab "shadows"- note that they're enabled by default and already nearly impossible to see in dark mode.
tabRow.height
: Change the height of the tab row.tabRow.underlineHeight
: Controls the height of a border placed between the tab row and the Terminal panes beneath it. This border doesn't exist currently.tabRow.underlineColor
: Controls the color of the aforementioned underlinewindow.frameColor
: TheDWMWA_BORDER_COLOR
DWM attribute is SUPER fun to play with, and trivial to set. We should definitely exposed it.