vscode的页面分为两部分,一部分是插件提供,一部分是主体。那么vscode在多主题实现上就要考虑把这两部分结合起来管理,相对来说要比单纯的网页实现多主题功能要复杂一些。
主体部分实现
我们先看下vscode主体部分样式是如何画出来了
registerThemingParticipant((theme, collector) => {
const activitybarTextForeground = theme.getColor(foreground);
if (activitybarTextForeground) {
/**
* color --> vscode原生
* background-color --> 插件
*/
collector.addRule(`
.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item .action-label {
color: ${activitybarTextForeground} !important;
}
`);
...
}
这是出于activitybar部分的样式代码,theme
获取颜色,collector
收集样式,最后都会插入到全局的style里面完成样式设置。
我们到getColor
方法里面看下
src/vs/workbench/services/themes/common/colorThemeData.ts
public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {
let color: Color | undefined = this.customColorMap[colorId];
if (color) {
return color;
}
color = this.colorMap[colorId];
if (useDefault !== false && types.isUndefined(color)) {
color = this.getDefault(colorId);
}
return color;
}
colorMap来至哪里呢,重点来了!
vscode的多主题其实都是通过主题插件来管理的
所有颜色都是取于themes目录下的json文件。
{
"type": "dark",
"colors": {
"dropdown.background": "#525252",
"list.activeSelectionBackground": "#707070",
"quickInputList.focusBackground": "#707070",
"list.inactiveSelectionBackground": "#4e4e4e",
...
}
}
这里面都是防止的颜色变量。
颜色的注册在大部分这个文件里面
src/vs/platform/theme/common/colorRegistry.ts
...
// 基础颜色
export const secondForeground = registerColor('second.foreground', { light: '#737373', dark: '#fff', hcDark: '#fff', hcLight: '#737373' }, nls.localize('secondForeground', "Color for text separators."));
...
vscode
的主题大概分为四类:深色主题、浅色主题、深色高亮主题、浅色高亮主题。
所以这里设置了四个默认值,如果插件里面取不到值的话就会找默认值。
好,这里是主体部分的颜色设置,下面我们看看插件里面的颜色设置。
插件部分
.search-warapper .searchInput .search-btn {
...
background-color: var(--vscode-productMain-color);
}
插件部分使用的是css的var函数。
CSS var函数
是一种自定义属性值的语法,用于插入自定义属性的值,而不是另一个属性的值的任何部分。它可以使您在不更改样式表的情况下更改样式,从而提高了可维护性和可重用性。 变量值继承至父类。
从主题插件里面获取到的样式全部写入了html的style里面。
这下就都懂了叭~
主题样式切换
唤醒颜色选择器
src/vs/workbench/contrib/themes/browser/themes.contribution.ts
override async run(accessor: ServicesAccessor) {
const themeService = accessor.get(IWorkbenchThemeService);
const installMessage = localize('installColorThemes', "Install Additional Color Themes...");
const browseMessage = '$(plus) ' + localize('browseColorThemes', "Browse Additional Color Themes...");
const placeholderMessage = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)");
const marketplaceTag = 'category:themes';
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setColorTheme(theme as IWorkbenchColorTheme, settingsTarget);
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceColorThemes(publisher, name, version);
const instantiationService = accessor.get(IInstantiationService);
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
const themes = await themeService.getColorThemes();
const currentTheme = themeService.getColorTheme();
const picks: QuickPickInput<ThemeItem>[] = [
...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")),
...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")),
...toEntries(themes.filter(t => isHighContrast(t.type)), localize('themes.category.hc', "high contrast themes")),
];
await picker.openQuickPick(picks, currentTheme);
}
上下选择的时候触发了这个事件
quickpick.onDidChangeActive(themes => selectTheme(themes[0]?.theme, false));
继续跟进去
src/vs/workbench/services/themes/browser/workbenchThemeService.ts
public setColorTheme(themeIdOrTheme: string | undefined | IWorkbenchColorTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchColorTheme | null> {
return this.colorThemeSequencer.queue(async () => {
return this.internalSetColorTheme(themeIdOrTheme, settingsTarget);
});
}
private async internalSetColorTheme(themeIdOrTheme: string | undefined | IWorkbenchColorTheme, settingsTarget: ThemeSettingTarget): Promise<IWorkbenchColorTheme | null> {
if (!themeIdOrTheme) {
return null;
}
const themeId = types.isString(themeIdOrTheme) ? validateThemeId(themeIdOrTheme) : themeIdOrTheme.id;
if (this.currentColorTheme.isLoaded && themeId === this.currentColorTheme.id) {
if (settingsTarget !== 'preview') {
this.currentColorTheme.toStorage(this.storageService);
}
return this.settings.setColorTheme(this.currentColorTheme, settingsTarget);
}
let themeData = this.colorThemeRegistry.findThemeById(themeId);
if (!themeData) {
if (themeIdOrTheme instanceof ColorThemeData) {
themeData = themeIdOrTheme;
} else {
return null;
}
}
try {
await themeData.ensureLoaded(this.extensionResourceLoaderService);
themeData.setCustomizations(this.settings);
return this.applyTheme(themeData, settingsTarget);
} catch (error) {
throw new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location?.toString(), error.message));
}
}
看看在themeData.ensureLoade方法里面做了什么
src/vs/workbench/services/themes/common/colorThemeData.ts
public ensureLoaded(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
return !this.isLoaded ? this.load(extensionResourceLoaderService) : Promise.resolve(undefined);
}
public reload(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
return this.load(extensionResourceLoaderService);
}
private load(extensionResourceLoaderService: IExtensionResourceLoaderService): Promise<void> {
if (!this.location) {
return Promise.resolve(undefined);
}
this.themeTokenColors = [];
this.clearCaches();
const result = {
colors: {},
textMateRules: [],
semanticTokenRules: [],
semanticHighlighting: false
};
return _loadColorTheme(extensionResourceLoaderService, this.location, result).then(_ => {
this.isLoaded = true;
this.semanticTokenRules = result.semanticTokenRules;
this.colorMap = result.colors; //生成colorMap
this.themeTokenColors = result.textMateRules;
this.themeSemanticHighlighting = result.semanticHighlighting;
});
}
上文中vscode主体部分colorMap就是在这里生成的
接着往下走
private updateDynamicCSSRules(themeData: IColorTheme) {
...
themingRegistry.getThemingParticipants().forEach(p => p(themeData, ruleCollector, this.environmentService));
const colorVariables: string[] = [];
for (const item of getColorRegistry().getColors()) {
const color = themeData.getColor(item.id, true);
if (color) {
colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
}
}
ruleCollector.addRule(`.monaco-workbench { ${colorVariables.join('\n')} }`);
_applyRules([...cssRules].join('\n'), colorThemeRulesClassName);
}
这里的
themingRegistry.getThemingParticipants().forEach(p => p(themeData, ruleCollector, this.environmentService));
很重要,这里重新调用了样式收集器代码,重新放进了collector里面
registerThemingParticipant((theme, collector) => {
const activitybarTextForeground = theme.getColor(foreground);
if (activitybarTextForeground) {
/**
* color --> vscode原生
* background-color --> 插件
*/
collector.addRule(`
.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item .action-label {
color: ${activitybarTextForeground} !important;
}
`);
...
}
src/vs/workbench/services/themes/browser/workbenchThemeService.ts
function _applyRules(styleSheetContent: string, rulesClassName: string) {
const themeStyles = document.head.getElementsByClassName(rulesClassName);
if (themeStyles.length === 0) {
const elStyle = document.createElement('style');
elStyle.type = 'text/css';
elStyle.className = rulesClassName;
elStyle.textContent = styleSheetContent;
document.head.appendChild(elStyle);
} else {
(<HTMLStyleElement>themeStyles[0]).textContent = styleSheetContent;
}
}
这里可以看到样式重新写入了style里。
主题切换完成!