standaloneThemeServiceImpl.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as dom from '../../../base/browser/dom.js';
  6. import { Color } from '../../../base/common/color.js';
  7. import { Emitter } from '../../../base/common/event.js';
  8. import { TokenizationRegistry, TokenMetadata } from '../../common/modes.js';
  9. import { TokenTheme, generateTokensCSSForColorMap } from '../../common/modes/supports/tokenization.js';
  10. import { hc_black, vs, vs_dark } from '../common/themes.js';
  11. import { Registry } from '../../../platform/registry/common/platform.js';
  12. import { asCssVariableName, Extensions } from '../../../platform/theme/common/colorRegistry.js';
  13. import { Extensions as ThemingExtensions } from '../../../platform/theme/common/themeService.js';
  14. import { Disposable } from '../../../base/common/lifecycle.js';
  15. import { ColorScheme } from '../../../platform/theme/common/theme.js';
  16. import { getIconsStyleSheet } from '../../../platform/theme/browser/iconsStyleSheet.js';
  17. const VS_THEME_NAME = 'vs';
  18. const VS_DARK_THEME_NAME = 'vs-dark';
  19. const HC_BLACK_THEME_NAME = 'hc-black';
  20. const colorRegistry = Registry.as(Extensions.ColorContribution);
  21. const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution);
  22. class StandaloneTheme {
  23. constructor(name, standaloneThemeData) {
  24. this.semanticHighlighting = false;
  25. this.themeData = standaloneThemeData;
  26. let base = standaloneThemeData.base;
  27. if (name.length > 0) {
  28. if (isBuiltinTheme(name)) {
  29. this.id = name;
  30. }
  31. else {
  32. this.id = base + ' ' + name;
  33. }
  34. this.themeName = name;
  35. }
  36. else {
  37. this.id = base;
  38. this.themeName = base;
  39. }
  40. this.colors = null;
  41. this.defaultColors = Object.create(null);
  42. this._tokenTheme = null;
  43. }
  44. get base() {
  45. return this.themeData.base;
  46. }
  47. notifyBaseUpdated() {
  48. if (this.themeData.inherit) {
  49. this.colors = null;
  50. this._tokenTheme = null;
  51. }
  52. }
  53. getColors() {
  54. if (!this.colors) {
  55. const colors = new Map();
  56. for (let id in this.themeData.colors) {
  57. colors.set(id, Color.fromHex(this.themeData.colors[id]));
  58. }
  59. if (this.themeData.inherit) {
  60. let baseData = getBuiltinRules(this.themeData.base);
  61. for (let id in baseData.colors) {
  62. if (!colors.has(id)) {
  63. colors.set(id, Color.fromHex(baseData.colors[id]));
  64. }
  65. }
  66. }
  67. this.colors = colors;
  68. }
  69. return this.colors;
  70. }
  71. getColor(colorId, useDefault) {
  72. const color = this.getColors().get(colorId);
  73. if (color) {
  74. return color;
  75. }
  76. if (useDefault !== false) {
  77. return this.getDefault(colorId);
  78. }
  79. return undefined;
  80. }
  81. getDefault(colorId) {
  82. let color = this.defaultColors[colorId];
  83. if (color) {
  84. return color;
  85. }
  86. color = colorRegistry.resolveDefaultColor(colorId, this);
  87. this.defaultColors[colorId] = color;
  88. return color;
  89. }
  90. defines(colorId) {
  91. return Object.prototype.hasOwnProperty.call(this.getColors(), colorId);
  92. }
  93. get type() {
  94. switch (this.base) {
  95. case VS_THEME_NAME: return ColorScheme.LIGHT;
  96. case HC_BLACK_THEME_NAME: return ColorScheme.HIGH_CONTRAST;
  97. default: return ColorScheme.DARK;
  98. }
  99. }
  100. get tokenTheme() {
  101. if (!this._tokenTheme) {
  102. let rules = [];
  103. let encodedTokensColors = [];
  104. if (this.themeData.inherit) {
  105. let baseData = getBuiltinRules(this.themeData.base);
  106. rules = baseData.rules;
  107. if (baseData.encodedTokensColors) {
  108. encodedTokensColors = baseData.encodedTokensColors;
  109. }
  110. }
  111. // Pick up default colors from `editor.foreground` and `editor.background` if available
  112. const editorForeground = this.themeData.colors['editor.foreground'];
  113. const editorBackground = this.themeData.colors['editor.background'];
  114. if (editorForeground || editorBackground) {
  115. const rule = { token: '' };
  116. if (editorForeground) {
  117. rule.foreground = editorForeground;
  118. }
  119. if (editorBackground) {
  120. rule.background = editorBackground;
  121. }
  122. rules.push(rule);
  123. }
  124. rules = rules.concat(this.themeData.rules);
  125. if (this.themeData.encodedTokensColors) {
  126. encodedTokensColors = this.themeData.encodedTokensColors;
  127. }
  128. this._tokenTheme = TokenTheme.createFromRawTokenTheme(rules, encodedTokensColors);
  129. }
  130. return this._tokenTheme;
  131. }
  132. getTokenStyleMetadata(type, modifiers, modelLanguage) {
  133. // use theme rules match
  134. const style = this.tokenTheme._match([type].concat(modifiers).join('.'));
  135. const metadata = style.metadata;
  136. const foreground = TokenMetadata.getForeground(metadata);
  137. const fontStyle = TokenMetadata.getFontStyle(metadata);
  138. return {
  139. foreground: foreground,
  140. italic: Boolean(fontStyle & 1 /* Italic */),
  141. bold: Boolean(fontStyle & 2 /* Bold */),
  142. underline: Boolean(fontStyle & 4 /* Underline */)
  143. };
  144. }
  145. }
  146. function isBuiltinTheme(themeName) {
  147. return (themeName === VS_THEME_NAME
  148. || themeName === VS_DARK_THEME_NAME
  149. || themeName === HC_BLACK_THEME_NAME);
  150. }
  151. function getBuiltinRules(builtinTheme) {
  152. switch (builtinTheme) {
  153. case VS_THEME_NAME:
  154. return vs;
  155. case VS_DARK_THEME_NAME:
  156. return vs_dark;
  157. case HC_BLACK_THEME_NAME:
  158. return hc_black;
  159. }
  160. }
  161. function newBuiltInTheme(builtinTheme) {
  162. let themeData = getBuiltinRules(builtinTheme);
  163. return new StandaloneTheme(builtinTheme, themeData);
  164. }
  165. export class StandaloneThemeServiceImpl extends Disposable {
  166. constructor() {
  167. super();
  168. this._onColorThemeChange = this._register(new Emitter());
  169. this.onDidColorThemeChange = this._onColorThemeChange.event;
  170. this._environment = Object.create(null);
  171. this._autoDetectHighContrast = true;
  172. this._knownThemes = new Map();
  173. this._knownThemes.set(VS_THEME_NAME, newBuiltInTheme(VS_THEME_NAME));
  174. this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
  175. this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
  176. const iconsStyleSheet = getIconsStyleSheet();
  177. this._codiconCSS = iconsStyleSheet.getCSS();
  178. this._themeCSS = '';
  179. this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
  180. this._globalStyleElement = null;
  181. this._styleElements = [];
  182. this._colorMapOverride = null;
  183. this.setTheme(VS_THEME_NAME);
  184. iconsStyleSheet.onDidChange(() => {
  185. this._codiconCSS = iconsStyleSheet.getCSS();
  186. this._updateCSS();
  187. });
  188. dom.addMatchMediaChangeListener('(forced-colors: active)', () => {
  189. this._updateActualTheme();
  190. });
  191. }
  192. registerEditorContainer(domNode) {
  193. if (dom.isInShadowDOM(domNode)) {
  194. return this._registerShadowDomContainer(domNode);
  195. }
  196. return this._registerRegularEditorContainer();
  197. }
  198. _registerRegularEditorContainer() {
  199. if (!this._globalStyleElement) {
  200. this._globalStyleElement = dom.createStyleSheet();
  201. this._globalStyleElement.className = 'monaco-colors';
  202. this._globalStyleElement.textContent = this._allCSS;
  203. this._styleElements.push(this._globalStyleElement);
  204. }
  205. return Disposable.None;
  206. }
  207. _registerShadowDomContainer(domNode) {
  208. const styleElement = dom.createStyleSheet(domNode);
  209. styleElement.className = 'monaco-colors';
  210. styleElement.textContent = this._allCSS;
  211. this._styleElements.push(styleElement);
  212. return {
  213. dispose: () => {
  214. for (let i = 0; i < this._styleElements.length; i++) {
  215. if (this._styleElements[i] === styleElement) {
  216. this._styleElements.splice(i, 1);
  217. return;
  218. }
  219. }
  220. }
  221. };
  222. }
  223. defineTheme(themeName, themeData) {
  224. if (!/^[a-z0-9\-]+$/i.test(themeName)) {
  225. throw new Error('Illegal theme name!');
  226. }
  227. if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {
  228. throw new Error('Illegal theme base!');
  229. }
  230. // set or replace theme
  231. this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));
  232. if (isBuiltinTheme(themeName)) {
  233. this._knownThemes.forEach(theme => {
  234. if (theme.base === themeName) {
  235. theme.notifyBaseUpdated();
  236. }
  237. });
  238. }
  239. if (this._theme.themeName === themeName) {
  240. this.setTheme(themeName); // refresh theme
  241. }
  242. }
  243. getColorTheme() {
  244. return this._theme;
  245. }
  246. setColorMapOverride(colorMapOverride) {
  247. this._colorMapOverride = colorMapOverride;
  248. this._updateThemeOrColorMap();
  249. }
  250. setTheme(themeName) {
  251. let theme;
  252. if (this._knownThemes.has(themeName)) {
  253. theme = this._knownThemes.get(themeName);
  254. }
  255. else {
  256. theme = this._knownThemes.get(VS_THEME_NAME);
  257. }
  258. this._desiredTheme = theme;
  259. this._updateActualTheme();
  260. }
  261. _updateActualTheme() {
  262. const theme = (this._autoDetectHighContrast && window.matchMedia(`(forced-colors: active)`).matches
  263. ? this._knownThemes.get(HC_BLACK_THEME_NAME)
  264. : this._desiredTheme);
  265. if (this._theme === theme) {
  266. // Nothing to do
  267. return;
  268. }
  269. this._theme = theme;
  270. this._updateThemeOrColorMap();
  271. }
  272. setAutoDetectHighContrast(autoDetectHighContrast) {
  273. this._autoDetectHighContrast = autoDetectHighContrast;
  274. this._updateActualTheme();
  275. }
  276. _updateThemeOrColorMap() {
  277. let cssRules = [];
  278. let hasRule = {};
  279. let ruleCollector = {
  280. addRule: (rule) => {
  281. if (!hasRule[rule]) {
  282. cssRules.push(rule);
  283. hasRule[rule] = true;
  284. }
  285. }
  286. };
  287. themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));
  288. const colorVariables = [];
  289. for (const item of colorRegistry.getColors()) {
  290. const color = this._theme.getColor(item.id, true);
  291. if (color) {
  292. colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
  293. }
  294. }
  295. ruleCollector.addRule(`.monaco-editor { ${colorVariables.join('\n')} }`);
  296. const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();
  297. ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));
  298. this._themeCSS = cssRules.join('\n');
  299. this._updateCSS();
  300. TokenizationRegistry.setColorMap(colorMap);
  301. this._onColorThemeChange.fire(this._theme);
  302. }
  303. _updateCSS() {
  304. this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
  305. this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);
  306. }
  307. getFileIconTheme() {
  308. return {
  309. hasFileIcons: false,
  310. hasFolderIcons: false,
  311. hidesExplorerArrows: false
  312. };
  313. }
  314. }