Skip to content

Commit 3e47084

Browse files
authored
feat: let users choose which themes apply in automatic light/dark mode (#2732)
When the automatic theme is selected, two dropdowns now appear in the theme picker letting users independently configure which theme resolves for light mode and which resolves for dark mode, instead of always defaulting to ui-light / ui-dark. Preferences are persisted under core.appearance.lightThemeName and core.appearance.darkThemeName and react to config changes immediately.
1 parent 6aeb84a commit 3e47084

3 files changed

Lines changed: 138 additions & 19 deletions

File tree

app/internal_packages/theme-picker/lib/theme-picker.tsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,34 @@ import React from 'react';
22

33
import { Flexbox, ScrollRegion } from 'mailspring-component-kit';
44
import { localized } from 'mailspring-exports';
5+
import {
6+
AUTOMATIC_THEME_NAME,
7+
LIGHT_THEME_NAME,
8+
DARK_THEME_NAME,
9+
} from '../../../src/theme-manager';
510
import ThemeOption, { toSelector } from './theme-option';
611
import { Disposable } from 'event-kit';
712

13+
// Sort order for built-in themes; community themes not in this list sort last.
14+
const INTERNAL_THEME_ORDER = [
15+
'ui-less-is-more',
16+
'ui-ubuntu',
17+
'ui-taiga',
18+
'ui-darkside',
19+
DARK_THEME_NAME,
20+
LIGHT_THEME_NAME,
21+
AUTOMATIC_THEME_NAME,
22+
];
23+
24+
function sortThemes<T extends { name: string }>(themes: T[]): T[] {
25+
return [...themes].sort(
26+
(a, b) => (INTERNAL_THEME_ORDER.indexOf(a.name) - INTERNAL_THEME_ORDER.indexOf(b.name)) * -1
27+
);
28+
}
29+
830
class ThemePicker extends React.Component<
931
Record<string, unknown>,
10-
{ themes: any[]; activeTheme: string }
32+
{ themes: any[]; activeTheme: string; lightTheme: string; darkTheme: string }
1133
> {
1234
static displayName = 'ThemePicker';
1335

@@ -33,6 +55,8 @@ class ThemePicker extends React.Component<
3355
return {
3456
themes: this.themes.getAvailableThemes(),
3557
activeTheme: this.themes.getActiveThemeSetting().name,
58+
lightTheme: this.themes.getConfiguredLightThemeName(),
59+
darkTheme: this.themes.getConfiguredDarkThemeName(),
3660
};
3761
}
3862

@@ -42,6 +66,16 @@ class ThemePicker extends React.Component<
4266
this._rewriteIFrame(prevActiveTheme, theme);
4367
}
4468

69+
_setLightTheme(themeName: string) {
70+
this.themes.setLightTheme(themeName);
71+
this.setState({ lightTheme: themeName });
72+
}
73+
74+
_setDarkTheme(themeName: string) {
75+
this.themes.setDarkTheme(themeName);
76+
this.setState({ darkTheme: themeName });
77+
}
78+
4579
_rewriteIFrame(prevActiveTheme: string, activeTheme: string) {
4680
const activeFrame = document.querySelector(`.${toSelector(activeTheme)}`) as HTMLIFrameElement;
4781
const prevActiveFrame = document.querySelector(
@@ -62,20 +96,7 @@ class ThemePicker extends React.Component<
6296
}
6397

6498
_renderThemeOptions() {
65-
const internalThemes = [
66-
'ui-less-is-more',
67-
'ui-ubuntu',
68-
'ui-taiga',
69-
'ui-darkside',
70-
'ui-dark',
71-
'ui-light',
72-
'ui-automatic',
73-
];
74-
const sortedThemes = [...this.state.themes];
75-
sortedThemes.sort((a, b) => {
76-
return (internalThemes.indexOf(a.name) - internalThemes.indexOf(b.name)) * -1;
77-
});
78-
return sortedThemes.map((theme) => (
99+
return sortThemes(this.state.themes).map((theme) => (
79100
<ThemeOption
80101
key={theme.name}
81102
theme={theme}
@@ -85,6 +106,46 @@ class ThemePicker extends React.Component<
85106
));
86107
}
87108

109+
_renderAutoSlots() {
110+
if (this.state.activeTheme !== AUTOMATIC_THEME_NAME) return null;
111+
112+
const sorted = sortThemes(this.state.themes.filter((t) => t.name !== AUTOMATIC_THEME_NAME));
113+
114+
return (
115+
<div className="auto-theme-selectors">
116+
<div className="auto-theme-selector">
117+
<label>
118+
<span className="auto-theme-icon"></span>
119+
{localized('When light')}
120+
</label>
121+
<select
122+
value={this.state.lightTheme}
123+
onChange={(e) => this._setLightTheme(e.target.value)}
124+
>
125+
{sorted.map((t) => (
126+
<option key={t.name} value={t.name}>
127+
{t.displayName}
128+
</option>
129+
))}
130+
</select>
131+
</div>
132+
<div className="auto-theme-selector">
133+
<label>
134+
<span className="auto-theme-icon"></span>
135+
{localized('When dark')}
136+
</label>
137+
<select value={this.state.darkTheme} onChange={(e) => this._setDarkTheme(e.target.value)}>
138+
{sorted.map((t) => (
139+
<option key={t.name} value={t.name}>
140+
{t.displayName}
141+
</option>
142+
))}
143+
</select>
144+
</div>
145+
</div>
146+
);
147+
}
148+
88149
render() {
89150
return (
90151
<div className="theme-picker">
@@ -94,6 +155,7 @@ class ThemePicker extends React.Component<
94155
{localized('Click any theme to apply:')}
95156
</div>
96157
<ScrollRegion style={{ margin: '10px 5px 0 5px', height: '300px' }}>
158+
{this._renderAutoSlots()}
97159
<Flexbox
98160
direction="row"
99161
height="auto"

app/internal_packages/theme-picker/styles/theme-picker.less

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,37 @@
1919
z-index: 0;
2020
}
2121
}
22+
.auto-theme-selectors {
23+
display: flex;
24+
flex-direction: row;
25+
align-items: flex-start;
26+
flex-wrap: wrap;
27+
gap: @padding-base-horizontal;
28+
padding: 0 @padding-base-horizontal @padding-base-vertical;
29+
30+
.auto-theme-selector {
31+
flex: 1;
32+
display: flex;
33+
flex-direction: column;
34+
align-items: flex-start;
35+
gap: 4px;
36+
37+
label {
38+
display: flex;
39+
align-items: center;
40+
gap: 4px;
41+
font-size: 11px;
42+
color: @text-color-very-subtle;
43+
cursor: default;
44+
}
45+
46+
select {
47+
width: 100%;
48+
font-size: @font-size-smaller;
49+
}
50+
}
51+
}
52+
2253
.create-theme {
2354
width: 100%;
2455
text-align: center;

app/src/theme-manager.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import PackageManager from './package-manager';
88

99
const CONFIG_THEME_KEY = 'core.theme';
1010
const CONFIG_USE_SYSTEM_ACCENT_KEY = 'core.appearance.useSystemAccent';
11+
const CONFIG_LIGHT_THEME_KEY = 'core.appearance.lightThemeName';
12+
const CONFIG_DARK_THEME_KEY = 'core.appearance.darkThemeName';
1113
const SYSTEM_ACCENT_SOURCE_PATH = 'system-accent:dynamic';
12-
const AUTOMATIC_THEME_NAME = 'ui-automatic';
13-
const LIGHT_THEME_NAME = 'ui-light';
14-
const DARK_THEME_NAME = 'ui-dark';
14+
export const AUTOMATIC_THEME_NAME = 'ui-automatic';
15+
export const LIGHT_THEME_NAME = 'ui-light';
16+
export const DARK_THEME_NAME = 'ui-dark';
1517

1618
function buildSystemAccentCSS(color: string): string {
1719
return `:root {
@@ -69,6 +71,12 @@ export default class ThemeManager {
6971

7072
AppEnv.config.onDidChange(CONFIG_THEME_KEY, () => this.updateThemePackageAndRecomputeLESS());
7173
AppEnv.config.onDidChange(CONFIG_USE_SYSTEM_ACCENT_KEY, () => this.applySystemAccent());
74+
AppEnv.config.onDidChange(CONFIG_LIGHT_THEME_KEY, () => {
75+
if (this.isAutomaticModeSelected()) this.updateThemePackageAndRecomputeLESS();
76+
});
77+
AppEnv.config.onDidChange(CONFIG_DARK_THEME_KEY, () => {
78+
if (this.isAutomaticModeSelected()) this.updateThemePackageAndRecomputeLESS();
79+
});
7280

7381
ipcRenderer.on('system-accent-color-changed', (_event, color: string | null) => {
7482
this._systemAccentColor = color;
@@ -158,12 +166,30 @@ export default class ThemeManager {
158166
}
159167
const configured = this.getConfiguredThemeName();
160168
if (configured === AUTOMATIC_THEME_NAME) {
161-
const resolvedName = this._systemDarkMode ? DARK_THEME_NAME : LIGHT_THEME_NAME;
169+
const resolvedName = this._systemDarkMode
170+
? this.getConfiguredDarkThemeName()
171+
: this.getConfiguredLightThemeName();
162172
return this.packageManager.getPackageNamed(resolvedName) || this.getBaseTheme();
163173
}
164174
return this.packageManager.getPackageNamed(configured) || this.getBaseTheme();
165175
}
166176

177+
getConfiguredLightThemeName(): string {
178+
return AppEnv.config.get(CONFIG_LIGHT_THEME_KEY) || LIGHT_THEME_NAME;
179+
}
180+
181+
getConfiguredDarkThemeName(): string {
182+
return AppEnv.config.get(CONFIG_DARK_THEME_KEY) || DARK_THEME_NAME;
183+
}
184+
185+
setLightTheme(packageName: string) {
186+
AppEnv.config.set(CONFIG_LIGHT_THEME_KEY, packageName);
187+
}
188+
189+
setDarkTheme(packageName: string) {
190+
AppEnv.config.set(CONFIG_DARK_THEME_KEY, packageName);
191+
}
192+
167193
// Returns the theme the user selected, which may be "ui-automatic" (unresolved).
168194
getActiveThemeSetting() {
169195
if (this.baseThemeOnly) {

0 commit comments

Comments
 (0)