From de7097ff1d8b2bd689316a1172393b6c98eda82a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 25 Sep 2024 07:35:27 -0700 Subject: [PATCH] Faster time-to-theme (#109) --- src-web/components/DefaultLayout.tsx | 8 +--- src-web/components/GlobalHooks.tsx | 12 +++-- src-web/components/ResponsePane.tsx | 6 ++- .../Settings/SettingsAppearance.tsx | 10 +++-- .../components/Settings/SettingsDesign.tsx | 6 +-- src-web/hooks/usePreferredAppearance.ts | 13 +----- src-web/hooks/useResolvedAppearance.ts | 9 +--- src-web/hooks/useResolvedTheme.ts | 24 ++++------ src-web/hooks/useSyncThemeToDocument.ts | 23 ---------- src-web/hooks/useThemes.ts | 13 ------ src-web/index.html | 1 + src-web/lib/theme/appearance.ts | 18 +++++++- src-web/lib/theme/themes.ts | 37 ++++++++++++++- src-web/theme.ts | 45 +++++++++++++++++++ 14 files changed, 132 insertions(+), 93 deletions(-) delete mode 100644 src-web/hooks/useSyncThemeToDocument.ts delete mode 100644 src-web/hooks/useThemes.ts create mode 100644 src-web/theme.ts diff --git a/src-web/components/DefaultLayout.tsx b/src-web/components/DefaultLayout.tsx index e81341b6..2bb5e110 100644 --- a/src-web/components/DefaultLayout.tsx +++ b/src-web/components/DefaultLayout.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import { motion } from 'framer-motion'; import { Outlet } from 'react-router-dom'; import { useOsInfo } from '../hooks/useOsInfo'; import { DialogProvider, Dialogs } from './DialogContext'; @@ -16,17 +15,14 @@ export function DefaultLayout() { - - + diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index d7ac6e20..a804e297 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -25,7 +25,6 @@ import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { settingsAtom, useSettings } from '../hooks/useSettings'; -import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { workspacesAtom } from '../hooks/useWorkspaces'; import { useZoom } from '../hooks/useZoom'; @@ -39,6 +38,11 @@ import { rosePineDefault } from '../lib/theme/themes/rose-pine'; import { yaakDark } from '../lib/theme/themes/yaak'; import { getThemeCSS } from '../lib/theme/window'; +export interface ModelPayload { + model: AnyModel; + windowLabel: string; +} + export function GlobalHooks() { // Include here so they always update, even if no component references them useRecentWorkspaces(); @@ -47,7 +51,6 @@ export function GlobalHooks() { useRecentRequests(); // Other useful things - useSyncThemeToDocument(); useNotificationToast(); useActiveWorkspaceChangedToast(); useEnsureActiveCookieJar(); @@ -61,11 +64,6 @@ export function GlobalHooks() { const queryClient = useQueryClient(); const { wasUpdatedExternally } = useRequestUpdateKey(null); - interface ModelPayload { - model: AnyModel; - windowLabel: string; - } - const setSettings = useSetAtom(settingsAtom); const setWorkspaces = useSetAtom(workspacesAtom); const setPlugins = useSetAtom(pluginsAtom); diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 1c21c13e..cf5e94a3 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -117,9 +117,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ > {activeResponse && ( {activeResponse.elapsed > 0 && ( diff --git a/src-web/components/Settings/SettingsAppearance.tsx b/src-web/components/Settings/SettingsAppearance.tsx index cb41b176..443fe6c6 100644 --- a/src-web/components/Settings/SettingsAppearance.tsx +++ b/src-web/components/Settings/SettingsAppearance.tsx @@ -3,10 +3,10 @@ import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { useResolvedAppearance } from '../../hooks/useResolvedAppearance'; import { useResolvedTheme } from '../../hooks/useResolvedTheme'; import { useSettings } from '../../hooks/useSettings'; -import { useThemes } from '../../hooks/useThemes'; import { useUpdateSettings } from '../../hooks/useUpdateSettings'; import { trackEvent } from '../../lib/analytics'; import { clamp } from '../../lib/clamp'; +import { getThemes } from '../../lib/theme/themes'; import { isThemeDark } from '../../lib/theme/window'; import type { ButtonProps } from '../core/Button'; import { Checkbox } from '../core/Checkbox'; @@ -52,12 +52,13 @@ const icons: IconProps['icon'][] = [ 'send_horizontal', ]; +const { themes } = getThemes(); + export function SettingsAppearance() { const workspace = useActiveWorkspace(); const settings = useSettings(); const updateSettings = useUpdateSettings(); const appearance = useResolvedAppearance(); - const { themes } = useThemes(); const activeTheme = useResolvedTheme(); if (settings == null || workspace == null) { @@ -161,9 +162,10 @@ export function SettingsAppearance() { space={3} className="mt-3 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto" > - - Theme Preview{' '} + + {activeTheme.active.name} + (preview) {buttonColors.map((c, i) => ( diff --git a/src-web/components/Settings/SettingsDesign.tsx b/src-web/components/Settings/SettingsDesign.tsx index 58c96929..60f9d5e0 100644 --- a/src-web/components/Settings/SettingsDesign.tsx +++ b/src-web/components/Settings/SettingsDesign.tsx @@ -1,9 +1,9 @@ import { open } from '@tauri-apps/plugin-dialog'; import React, { useState } from 'react'; import { useLocalStorage } from 'react-use'; -import { useThemes } from '../../hooks/useThemes'; import { capitalize } from '../../lib/capitalize'; import { invokeCmd } from '../../lib/tauri'; +import { getThemes } from '../../lib/theme/themes'; import { yaakDark } from '../../lib/theme/themes/yaak'; import { getThemeCSS } from '../../lib/theme/window'; import { Banner } from '../core/Banner'; @@ -45,9 +45,9 @@ const icons: IconProps['icon'][] = [ 'send_horizontal', ]; -export function SettingsDesign() { - const themes = useThemes(); +const themes = getThemes(); +export function SettingsDesign() { const [exportDir, setExportDir] = useLocalStorage('theme_export_dir', null); const [loadingExport, setLoadingExport] = useState(false); diff --git a/src-web/hooks/usePreferredAppearance.ts b/src-web/hooks/usePreferredAppearance.ts index 50fa40f0..669f4b78 100644 --- a/src-web/hooks/usePreferredAppearance.ts +++ b/src-web/hooks/usePreferredAppearance.ts @@ -1,18 +1,9 @@ import { useEffect, useState } from 'react'; import type { Appearance } from '../lib/theme/appearance'; -import { - getCSSAppearance, - getWindowAppearance, - subscribeToWindowAppearanceChange, -} from '../lib/theme/appearance'; +import { getCSSAppearance, subscribeToPreferredAppearance } from '../lib/theme/appearance'; export function usePreferredAppearance() { const [preferredAppearance, setPreferredAppearance] = useState(getCSSAppearance()); - - useEffect(() => { - getWindowAppearance().then(setPreferredAppearance); - return subscribeToWindowAppearanceChange(setPreferredAppearance); - }, []); - + useEffect(() => subscribeToPreferredAppearance(setPreferredAppearance), []); return preferredAppearance; } diff --git a/src-web/hooks/useResolvedAppearance.ts b/src-web/hooks/useResolvedAppearance.ts index 7702fb02..92fc54ba 100644 --- a/src-web/hooks/useResolvedAppearance.ts +++ b/src-web/hooks/useResolvedAppearance.ts @@ -1,14 +1,9 @@ +import { resolveAppearance } from '../lib/theme/appearance'; import { usePreferredAppearance } from './usePreferredAppearance'; import { useSettings } from './useSettings'; export function useResolvedAppearance() { const preferredAppearance = usePreferredAppearance(); - const settings = useSettings(); - const appearance = - settings == null || settings?.appearance === 'system' - ? preferredAppearance - : settings.appearance; - - return appearance; + return resolveAppearance(preferredAppearance, settings.appearance); } diff --git a/src-web/hooks/useResolvedTheme.ts b/src-web/hooks/useResolvedTheme.ts index 47e79144..9b29099a 100644 --- a/src-web/hooks/useResolvedTheme.ts +++ b/src-web/hooks/useResolvedTheme.ts @@ -1,20 +1,14 @@ -import { isThemeDark } from '../lib/theme/window'; -import { useResolvedAppearance } from './useResolvedAppearance'; +import { getResolvedTheme } from '../lib/theme/themes'; +import { usePreferredAppearance } from './usePreferredAppearance'; import { useSettings } from './useSettings'; -import { useThemes } from './useThemes'; export function useResolvedTheme() { - const appearance = useResolvedAppearance(); + const preferredAppearance = usePreferredAppearance(); const settings = useSettings(); - const { themes, fallback } = useThemes(); - - const darkThemes = themes.filter((t) => isThemeDark(t)); - const lightThemes = themes.filter((t) => !isThemeDark(t)); - - const dark = darkThemes.find((t) => t.id === settings?.themeDark) ?? fallback.dark; - const light = lightThemes.find((t) => t.id === settings?.themeLight) ?? fallback.light; - - const active = appearance === 'dark' ? dark : light; - - return { dark, light, active }; + return getResolvedTheme( + preferredAppearance, + settings.appearance, + settings.themeLight, + settings.themeDark, + ); } diff --git a/src-web/hooks/useSyncThemeToDocument.ts b/src-web/hooks/useSyncThemeToDocument.ts deleted file mode 100644 index d7089456..00000000 --- a/src-web/hooks/useSyncThemeToDocument.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { emit } from '@tauri-apps/api/event'; -import { useEffect } from 'react'; -import type { YaakTheme } from '../lib/theme/window'; -import { addThemeStylesToDocument, setThemeOnDocument } from '../lib/theme/window'; -import { useResolvedTheme } from './useResolvedTheme'; - -export function useSyncThemeToDocument() { - const theme = useResolvedTheme(); - - useEffect(() => { - setThemeOnDocument(theme.active); - emitBgChange(theme.active); - }, [theme.active]); - - useEffect(() => { - addThemeStylesToDocument(theme.active); - }, [theme.active]); -} - -function emitBgChange(t: YaakTheme) { - if (t.surface == null) return; - emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error); -} diff --git a/src-web/hooks/useThemes.ts b/src-web/hooks/useThemes.ts deleted file mode 100644 index eb2b7123..00000000 --- a/src-web/hooks/useThemes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defaultDarkTheme, defaultLightTheme, yaakThemes } from '../lib/theme/themes'; - -export function useThemes() { - const dark = defaultDarkTheme; - const light = defaultLightTheme; - - const otherThemes = yaakThemes - .filter((t) => t.id !== dark.id && t.id !== light.id) - .sort((a, b) => a.name.localeCompare(b.name)); - - const themes = [dark, light, ...otherThemes]; - return { themes, fallback: { dark, light } }; -} diff --git a/src-web/index.html b/src-web/index.html index 8cfafc4c..9dd320c5 100644 --- a/src-web/index.html +++ b/src-web/index.html @@ -26,6 +26,7 @@
+ diff --git a/src-web/lib/theme/appearance.ts b/src-web/lib/theme/appearance.ts index ec2cb370..4d199e90 100644 --- a/src-web/lib/theme/appearance.ts +++ b/src-web/lib/theme/appearance.ts @@ -18,7 +18,9 @@ export async function getWindowAppearance(): Promise { export function subscribeToWindowAppearanceChange( cb: (appearance: Appearance) => void, ): () => void { - const container = { unsubscribe: () => {} }; + const container = { + unsubscribe: () => {}, + }; getCurrentWebviewWindow() .onThemeChanged((t) => { @@ -30,3 +32,17 @@ export function subscribeToWindowAppearanceChange( return () => container.unsubscribe(); } + +export function resolveAppearance( + preferredAppearance: Appearance, + appearanceSetting: string, +): Appearance { + const appearance = appearanceSetting === 'system' ? preferredAppearance : appearanceSetting; + return appearance === 'dark' ? 'dark' : 'light'; +} + +export function subscribeToPreferredAppearance(cb: (a: Appearance) => void) { + cb(getCSSAppearance()); + getWindowAppearance().then(cb); + subscribeToWindowAppearanceChange(cb); +} diff --git a/src-web/lib/theme/themes.ts b/src-web/lib/theme/themes.ts index 6a84fa50..cce894f3 100644 --- a/src-web/lib/theme/themes.ts +++ b/src-web/lib/theme/themes.ts @@ -1,3 +1,5 @@ +import type { Appearance } from './appearance'; +import { resolveAppearance } from './appearance'; import { catppuccin } from './themes/catppuccin'; import { github } from './themes/github'; import { hotdogStand } from './themes/hotdog-stand'; @@ -5,11 +7,12 @@ import { monokaiPro } from './themes/monokai-pro'; import { relaxing } from './themes/relaxing'; import { rosePine } from './themes/rose-pine'; import { yaak, yaakDark, yaakLight } from './themes/yaak'; +import { isThemeDark } from './window'; export const defaultDarkTheme = yaakDark; export const defaultLightTheme = yaakLight; -export const yaakThemes = [ +const allThemes = [ ...yaak, ...catppuccin, ...relaxing, @@ -18,3 +21,35 @@ export const yaakThemes = [ ...monokaiPro, ...hotdogStand, ]; + +export function getThemes() { + const dark = defaultDarkTheme; + const light = defaultLightTheme; + + const otherThemes = allThemes + .filter((t) => t.id !== dark.id && t.id !== light.id) + .sort((a, b) => a.name.localeCompare(b.name)); + + const themes = [dark, light, ...otherThemes]; + return { themes, fallback: { dark, light } }; +} + +export function getResolvedTheme( + preferredAppearance: Appearance, + appearanceSetting: string, + themeLight: string, + themeDark: string, +) { + const appearance = resolveAppearance(preferredAppearance, appearanceSetting); + const { themes, fallback } = getThemes(); + + const darkThemes = themes.filter((t) => isThemeDark(t)); + const lightThemes = themes.filter((t) => !isThemeDark(t)); + + const dark = darkThemes.find((t) => t.id === themeDark) ?? fallback.dark; + const light = lightThemes.find((t) => t.id === themeLight) ?? fallback.light; + + const active = appearance === 'dark' ? dark : light; + + return { dark, light, active }; +} diff --git a/src-web/theme.ts b/src-web/theme.ts new file mode 100644 index 00000000..990f0c93 --- /dev/null +++ b/src-web/theme.ts @@ -0,0 +1,45 @@ +import { emit, listen } from '@tauri-apps/api/event'; +import type { ModelPayload } from './components/GlobalHooks'; +import { getSettings } from './lib/store'; +import type { + Appearance} from './lib/theme/appearance'; +import { + getCSSAppearance, + subscribeToPreferredAppearance, +} from './lib/theme/appearance'; +import { getResolvedTheme } from './lib/theme/themes'; +import type { YaakTheme } from './lib/theme/window'; +import { addThemeStylesToDocument, setThemeOnDocument } from './lib/theme/window'; + +// NOTE: CSS appearance isn't as accurate as getting it async from the window (next step), but we want +// a good appearance guess so we're not waiting too long +let preferredAppearance: Appearance = getCSSAppearance(); +subscribeToPreferredAppearance(async (a) => { + preferredAppearance = a; + await configureTheme(); +}); +configureTheme().catch(console.error); + +// Listen for settings changes, the re-compute theme +listen('upserted_model', async (event) => { + if (event.payload.model.model !== 'settings') return; + await configureTheme(); +}).catch(console.error); + +async function configureTheme() { + const settings = await getSettings(); + const theme = getResolvedTheme( + preferredAppearance, + settings.appearance, + settings.themeLight, + settings.themeDark, + ); + addThemeStylesToDocument(theme.active); + setThemeOnDocument(theme.active); + emitBgChange(theme.active); +} + +function emitBgChange(t: YaakTheme) { + if (t.surface == null) return; + emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error); +}