Файли
openidea/src/workbench/theme.ts
T

123 рядки
5.1 KiB
TypeScript
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/** Applies `data-theme` and the accent colour from appearance settings. */
import { useEffect, useSyncExternalStore } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { create } from "zustand";
import { call, commands, isTauri } from "@/platform/ipc";
import { useProject } from "@/platform/project";
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
/** PyCharm's brand blue: the accent both Islands themes ship with. */
export const DEFAULT_ACCENT = "#3871e1";
/** Accent presets (Settings › Appearance, Welcome › Customize); the first is the Islands default. */
export const ACCENT_PRESETS = [
{ value: DEFAULT_ACCENT, label: "Blue" },
{ value: "#24a394", label: "Teal" },
{ value: "#955ae0", label: "Purple" },
{ value: "#e08855", label: "Orange" },
] as const;
/** Accents that mean "use the theme's own" (the current default and the pre-Islands one). */
const THEME_ACCENTS = new Set([DEFAULT_ACCENT, "#8fa5d6"]);
/** Session override used when settings can't be persisted (no backend). */
export const useThemeOverride = create<{ theme: ThemeSetting | null }>(() => ({ theme: null }));
const media = () =>
typeof window !== "undefined" && window.matchMedia ? window.matchMedia("(prefers-color-scheme: light)") : null;
function subscribeScheme(cb: () => void) {
const m = media();
m?.addEventListener?.("change", cb);
return () => m?.removeEventListener?.("change", cb);
}
const prefersLight = () => !!media()?.matches;
interface AppearanceLike {
theme?: string;
syncWithOs?: boolean;
preferredLightTheme?: string;
preferredDarkTheme?: string;
}
/**
* Concrete theme id: the chosen theme, or with "Sync with OS" (legacy
* `theme = "system"`) the preferred light / dark theme for the OS mode.
*/
export function resolveThemeId(a: AppearanceLike | undefined, systemLight: boolean): "dark" | "light" | "dark-hc" {
const concrete = (v: string | undefined, d: "dark" | "light") => (v === "dark" || v === "light" || v === "dark-hc" ? v : d);
if (a?.syncWithOs || a?.theme === "system")
return systemLight ? concrete(a?.preferredLightTheme, "light") : concrete(a?.preferredDarkTheme, "dark");
return concrete(a?.theme, "light");
}
/** `dark-hc` resolves to `dark` plus `data-contrast="high"`. */
export function resolveTheme(setting: string | undefined, systemLight: boolean): "dark" | "light" {
return resolveThemeId({ theme: setting }, systemLight) === "light" ? "light" : "dark";
}
const ACCENT_RE = /^#[0-9a-f]{3,8}$/i;
export function useApplyTheme() {
const appearance = useProject((s) => s.settings?.appearance);
const override = useThemeOverride((s) => s.theme);
const systemLight = useSyncExternalStore(subscribeScheme, prefersLight, () => false);
const id = override
? resolveThemeId({ theme: override }, systemLight)
: resolveThemeId(appearance as AppearanceLike | undefined, systemLight);
const theme = id === "light" ? "light" : "dark";
const accent = appearance?.accent;
// Until settings arrive, keep the theme public/boot.js restored rather than the light default.
const pending = !override && !appearance;
useEffect(() => {
const root = document.documentElement;
if (pending && root.dataset.theme) return;
root.dataset.theme = theme;
if (id === "dark-hc") root.dataset.contrast = "high";
else delete root.dataset.contrast;
// Each theme ships its own accent; only a deliberate choice overrides it.
const custom = !!accent && ACCENT_RE.test(accent) && !THEME_ACCENTS.has(accent.toLowerCase());
if (custom) root.style.setProperty("--accent", accent);
else root.style.removeProperty("--accent");
try {
localStorage.setItem("openidea.theme", JSON.stringify({ theme, contrast: root.dataset.contrast }));
} catch {
/* storage unavailable */
}
syncWindowBackground();
}, [pending, theme, id, accent]);
}
/** The native window shows its own colour where a live resize outruns the webview; match the frame. */
function syncWindowBackground() {
if (!isTauri()) return;
const color = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
if (!color) return;
getCurrentWindow()
.setBackgroundColor(color)
.catch(() => {});
}
export function currentTheme(): "dark" | "light" {
const override = useThemeOverride.getState().theme;
const id = override
? resolveThemeId({ theme: override }, prefersLight())
: resolveThemeId(useProject.getState().settings?.appearance as AppearanceLike | undefined, prefersLight());
return id === "light" ? "light" : "dark";
}
export async function toggleTheme() {
const next: ThemeSetting = currentTheme() === "dark" ? "light" : "dark";
useThemeOverride.setState({ theme: next });
if (!isTauri()) return;
try {
let settings = await call(commands.settingsSet("global", "appearance.theme", JSON.stringify(next)));
// An explicit choice ends "Sync with OS", as in IDEA.
if (settings.appearance?.syncWithOs) settings = await call(commands.settingsSet("global", "appearance.syncWithOs", "false"));
useProject.setState({ settings });
useThemeOverride.setState({ theme: null });
} catch {
/* keep the session override */
}
}