diff --git a/src/editor/core/colorSchemeRuntime.ts b/src/editor/core/colorSchemeRuntime.ts index d6aa7221..41e3a68f 100644 --- a/src/editor/core/colorSchemeRuntime.ts +++ b/src/editor/core/colorSchemeRuntime.ts @@ -1,11 +1,13 @@ /** * Applies Settings › Editor › Color Scheme (scheme + attribute overrides) as one * stylesheet, and Editor › Font › Typography Settings as the root's - * `--fw-code` / `--fw-code-bold`. + * `--fw-code` / `--fw-code-bold`. When the IDE theme turns light or dark, a chosen + * scheme of the other kind is switched back to the theme's own. */ import { useProject } from "@/platform/project"; import { editorPrefs, fontWeightVars } from "@/platform/editorSettings"; -import { schemeCss } from "@/platform/colorScheme"; +import { schemeAfterThemeChange, schemeCss } from "@/platform/colorScheme"; +import { call, commands, isTauri } from "@/platform/ipc"; export function installColorScheme(): () => void { if (typeof document === "undefined") return () => {}; @@ -21,11 +23,36 @@ export function installColorScheme(): () => void { for (const [name, value] of Object.entries(fontWeightVars(prefs.fontWeight, prefs.boldFontWeight))) if (root.style.getPropertyValue(name) !== value) root.style.setProperty(name, value); }; + let kind = root.dataset.theme; + const followTheme = () => { + const next = root.dataset.theme; + if (next === kind) return; + const previous = kind; + kind = next; + const settings = useProject.getState().settings; + if (!previous || !settings || (next !== "light" && next !== "dark")) return; + const scheme = schemeAfterThemeChange(settings.editor?.colorScheme, next, editorPrefs(settings).customSchemes); + if (scheme === null) return; + if (!isTauri()) { + useProject.setState({ settings: { ...settings, editor: { ...settings.editor, colorScheme: scheme } } }); + return; + } + call(commands.settingsSet("global", "editor.colorScheme", JSON.stringify(scheme))).then( + (updated) => useProject.setState({ settings: updated }), + (e) => console.warn("[color scheme] cannot follow the theme", e), + ); + }; apply(); const unsubscribe = useProject.subscribe((s, prev) => { if (s.settings !== prev.settings) apply(); }); - const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(apply) : null; + const observer = + typeof MutationObserver !== "undefined" + ? new MutationObserver(() => { + apply(); + followTheme(); + }) + : null; observer?.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-contrast", "data-color-theme", "data-plugin-theme"], diff --git a/src/platform/colorScheme.test.ts b/src/platform/colorScheme.test.ts index ade55938..67cecc22 100644 --- a/src/platform/colorScheme.test.ts +++ b/src/platform/colorScheme.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { beforeAll, describe, expect, it } from "vitest"; -import { _setSchemeSource, schemeCss, schemeTokens } from "./colorScheme"; +import { _setSchemeSource, schemeAfterThemeChange, schemeCss, schemeTokens } from "./colorScheme"; import { fontWeightVars, validateEditorExtras } from "./editorSettings"; import type { Settings } from "./ipc"; import { useProject } from "./project"; @@ -66,6 +66,39 @@ describe("color schemes", () => { } }); + it("switches a chosen scheme of the other kind back to the theme's own when the theme turns light or dark", async () => { + expect(schemeAfterThemeChange("one-dark", "light")).toBe("theme-default"); + expect(schemeAfterThemeChange("solarized-light", "light")).toBeNull(); + expect(schemeAfterThemeChange("theme-default", "light")).toBeNull(); + const customs = { "custom-night": { name: "Night", parent: "dark" as const, attributes: {} } }; + expect(schemeAfterThemeChange("custom-night", "light", customs)).toBe("theme-default"); + expect(schemeAfterThemeChange("custom-night", "dark", customs)).toBeNull(); + + const root = document.documentElement; + root.dataset.theme = "dark"; + const previous = useProject.getState().settings; + const scheme = () => useProject.getState().settings?.editor?.colorScheme; + useProject.setState({ settings: { editor: { colorScheme: "one-dark" } } as unknown as Settings }); + const dispose = installColorScheme(); + try { + root.dataset.theme = "light"; + await Promise.resolve(); + expect(scheme()).toBe("theme-default"); + // A dark scheme chosen under the light theme stays until the theme changes again. + useProject.setState({ settings: { editor: { colorScheme: "dark" } } as unknown as Settings }); + root.dataset.colorTheme = "solarized-light"; + await Promise.resolve(); + expect(scheme()).toBe("dark"); + root.dataset.theme = "dark"; + await Promise.resolve(); + expect(scheme()).toBe("dark"); + } finally { + dispose(); + delete root.dataset.colorTheme; + useProject.setState({ settings: previous }); + } + }); + it("keeps editor weights out of the schemes", () => { for (const id of ["light", "dark", "dark-hc"] as const) { expect(schemeTokens(id).has("--fw-code")).toBe(false); diff --git a/src/platform/colorScheme.ts b/src/platform/colorScheme.ts index 8542617e..b9349d3b 100644 --- a/src/platform/colorScheme.ts +++ b/src/platform/colorScheme.ts @@ -259,6 +259,25 @@ export function effectiveScheme( return themeScheme(root); } +/** Light or dark: a built-in scheme's theme kind, a custom scheme's parent (null: unknown). */ +export function schemeKind(id: string | undefined, customs: CustomColorSchemes = {}): "light" | "dark" | null { + if (isBuiltinTheme(id)) return themeKind(id); + return (id && customSchemes(customs)[id]?.parent) || null; +} + +/** + * The scheme setting once the IDE theme has turned `kind`, or null to keep it: a chosen scheme + * of the other kind gives way to the theme's own, so a light theme doesn't keep a dark editor. + */ +export function schemeAfterThemeChange( + setting: string | undefined, + kind: "light" | "dark", + customs: CustomColorSchemes = {}, +): string | null { + const current = schemeKind(setting, customs); + return current && current !== kind ? "theme-default" : null; +} + /** Scheme belonging to the active IDE theme (`data-theme` / `data-contrast`). */ export function themeScheme( root: HTMLElement | null = typeof document !== "undefined" ? document.documentElement : null,