diff --git a/docfiles/themes/high-contrast.json b/docfiles/themes/high-contrast.json index 1d2988768a44..75bbab116a2e 100644 --- a/docfiles/themes/high-contrast.json +++ b/docfiles/themes/high-contrast.json @@ -1,6 +1,7 @@ { "id": "high-contrast", "name": "High Contrast", + "monacoBaseTheme": "hc-black", "overrideFiles": [ "/docfiles/themes/overrides/high-contrast-overrides.css" ], diff --git a/react-common/components/theming/themeManager.ts b/react-common/components/theming/themeManager.ts index acde0ea5c80d..42a30636d1c5 100644 --- a/react-common/components/theming/themeManager.ts +++ b/react-common/components/theming/themeManager.ts @@ -2,9 +2,15 @@ export interface ThemeInfo { id: string; name: string; overrides?: string; + monacoBaseTheme?: string; // https://code.visualstudio.com/docs/getstarted/themes colors: { [key: string]: string }; } +export interface ThemeChangeSubscriber { + subscriberId: string; + onThemeChange: () => void; +} + /* * Singleton class to handle theming operations, like loading and switching themes. */ @@ -12,6 +18,7 @@ export class ThemeManager { private static instance: ThemeManager; private themes: ThemeInfo[]; private activeTheme: ThemeInfo; + private subscribers: ThemeChangeSubscriber[]; private constructor() {} // private to ensure singleton @@ -46,6 +53,7 @@ export class ThemeManager { const theme: ThemeInfo = { id: themeData.id, name: themeData.name, + monacoBaseTheme: themeData.monacoBaseTheme, colors: themeData.colors }; @@ -68,8 +76,8 @@ export class ThemeManager { } } - public getActiveThemeId(): string { - return this.activeTheme?.id; + public getActiveTheme(): Readonly { + return this.activeTheme; } public async getAllThemes(): Promise { @@ -81,7 +89,7 @@ export class ThemeManager { } public async switchTheme(themeId: string) { - if (themeId === this.getActiveThemeId()) { + if (themeId === this.getActiveTheme()?.id) { return; } @@ -103,7 +111,30 @@ export class ThemeManager { // textContent is safer than innerHTML, less vulnerable to XSS styleElement.textContent = `.pxt-theme-root { ${themeAsStyle} }`; + + this.activeTheme = theme; + this.notifySubscribers(); + } + } + + public subscribe(subscriberId: string, onThemeChange: () => void) { + if (this.subscribers?.some(s => s.subscriberId === subscriberId)) { + return; } + + if (!this.subscribers) { + this.subscribers = []; + } + + this.subscribers.push({ subscriberId, onThemeChange }); + } + + public unsubscribe(subscriberId: string) { + this.subscribers = this.subscribers.filter(s => s.subscriberId !== subscriberId); + } + + private notifySubscribers() { + this.subscribers.forEach(s => s.onThemeChange()); } } diff --git a/theme/monaco.less b/theme/monaco.less index b637ed130fe4..f6678a858046 100644 --- a/theme/monaco.less +++ b/theme/monaco.less @@ -32,23 +32,33 @@ Monaco Editor *******************************/ +.monaco-editor { + background-color: var(--pxt-target-background1) !important; + color: var(--pxt-target-foreground1) !important; + + .monaco-editor-background, .margin { + background-color: var(--pxt-target-background1) !important; + color: var(--pxt-target-foreground1) !important; + } +} + .monaco-editor .monaco-scrollable-element.editor-scrollable { margin-left: .5rem; } /* Monaco Editor Line number colors, changed from the default light theme */ .monaco-editor.vs .line-numbers { - color: #2c3e50 !important; + color: var(--pxt-target-foreground1) !important; } /* Monaco Editor Line number background, changed from the default light theme */ .monaco-editor.vs .margin-view-overlays.monaco-editor-background { - background: rgba(0, 0, 0, 0.05) !important; + background: var(--pxt-neutral-alpha05) !important; } /* Monaco Editor Current line highlighting, only needed for the light theme */ .monaco-editor.vs .current-line { - background: rgba(0, 0, 255, 0.1) !important; + background: var(--pxt-colors-blue-alpha10) !important; } /* Monaco Editor Suggest Widget, styling */ @@ -57,17 +67,17 @@ z-index: @monacoSuggestZIndex !important; } .monaco-list-row.focused { - background-color:#0078D7 !important; - color: #fff !important; + background-color: var(--pxt-colors-blue-background) !important; + color: var(--pxt-colors-blue-foreground) !important; } .monaco-editor.vs .suggest-widget:not(.frozen) .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { - color: #0078D7 !important; + color: var(--pxt-colors-blue-background) !important; } .monaco-editor.vs .suggest-widget:not(.frozen) .monaco-list .monaco-list-row.focused .monaco-highlighted-label .highlight { - color: #fff !important; + color: var(--pxt-colors-blue-foreground) !important; } .monaco-editor.vs .suggest-widget .details > .monaco-scrollable-element > .body > .type, .monaco-editor .suggest-widget .monaco-list .monaco-list-row .type-label { - color: #FFB900 !important; + color: var(--pxt-colors-orange-background) !important; } /* Monaco Editor Rename Widget: default positioning causes scroll with transparent editor toolbar, so we reposition */ @@ -82,8 +92,8 @@ /* Monaco Editor Main tokens */ .monaco-editor .highlight-statement { - background-color: #b8b8fd; - color: black !important; + background-color: var(--pxt-target-background-hover1); + color: var(--pxt-target-foreground-hover1) !important; } /* Monaco Toolbox */ diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 0423566a089f..0bfe159c09be 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -6197,7 +6197,7 @@ document.addEventListener("DOMContentLoaded", async () => { const defaultColorThemeId = pxt.appTarget?.appTheme?.defaultColorTheme; if (defaultColorThemeId) { const themeManager = ThemeManager.getInstance(); - if (defaultColorThemeId !== themeManager.getActiveThemeId()) { + if (defaultColorThemeId !== themeManager.getActiveTheme()?.id) { return themeManager.switchTheme(defaultColorThemeId); } } diff --git a/webapp/src/monaco.tsx b/webapp/src/monaco.tsx index 01177842f525..c018e880c209 100644 --- a/webapp/src/monaco.tsx +++ b/webapp/src/monaco.tsx @@ -30,6 +30,7 @@ import IProjectView = pxt.editor.IProjectView; import ErrorListState = pxt.editor.ErrorListState; import * as pxtblockly from "../../pxtblocks"; +import { ThemeChangeSubscriber, ThemeManager } from "../../react-common/components/theming/themeManager"; const MIN_EDITOR_FONT_SIZE = 10 const MAX_EDITOR_FONT_SIZE = 40 @@ -390,6 +391,8 @@ export class Editor extends toolboxeditor.ToolboxEditor { this.onUserPreferencesChanged = this.onUserPreferencesChanged.bind(this); data.subscribe(this.userPreferencesSubscriber, auth.HIGHCONTRAST); + + ThemeManager.getInstance()?.subscribe("monaco", () => this.onUserPreferencesChanged()); } onUserPreferencesChanged() { @@ -747,8 +750,11 @@ export class Editor extends toolboxeditor.ToolboxEditor { } const colors = pxt.appTarget.appTheme.monacoColors || {}; + const baseTheme: monaco.editor.BuiltinTheme = hc + ? "hc-black" + : (ThemeManager.getInstance()?.getActiveTheme()?.monacoBaseTheme as monaco.editor.BuiltinTheme) ?? (inverted ? "vs-dark" : "vs"); monaco.editor.defineTheme('pxtTheme', { - base: hc ? 'hc-black' : (inverted ? 'vs-dark' : 'vs'), // can also be vs-dark or hc-black + base: baseTheme, inherit: true, // can also be false to completely replace the builtin rules rules: rules, colors: hc ? {} : colors @@ -2109,15 +2115,9 @@ export class Editor extends toolboxeditor.ToolboxEditor { } protected getEditorColor() { - if (pxt.appTarget.appTheme.monacoColors && pxt.appTarget.appTheme.monacoColors["editor.background"]) { - return pxt.appTarget.appTheme.monacoColors["editor.background"] - } - else if (pxt.appTarget.appTheme.invertedMonaco) { - return "#1e1e1e" - } - else { - return "#ffffff" - } + // Editor color is set to var(--pxt-target-background1), which can change. + // This is not ideal, but just return empty for now. + return ""; } }