Make engine stream react to prefers-color-scheme
media query change (#4008)
* Add event listener for theme media query to update engine theme * tsc fixes * Add a Playwright test for UI and engine theme switch * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -48,6 +48,11 @@ export const TEST_SETTINGS_ONBOARDING_START = {
|
||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '' },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_DEFAULT_THEME = {
|
||||
...TEST_SETTINGS,
|
||||
app: { ...TEST_SETTINGS.app, theme: Themes.System },
|
||||
} satisfies Partial<SaveSettingsPayload>
|
||||
|
||||
export const TEST_SETTINGS_CORRUPTED = {
|
||||
app: {
|
||||
theme: Themes.Dark,
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
TEST_SETTINGS_KEY,
|
||||
TEST_SETTINGS_CORRUPTED,
|
||||
TEST_SETTINGS,
|
||||
TEST_SETTINGS_DEFAULT_THEME,
|
||||
} from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
|
||||
@ -656,6 +657,60 @@ const extrude001 = extrude(5, sketch001)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
|
||||
page,
|
||||
}) => {
|
||||
// Override the settings so that the theme is set to `system`
|
||||
await page.addInitScript(
|
||||
({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
},
|
||||
{
|
||||
settingsKey: TEST_SETTINGS_KEY,
|
||||
settings: TOML.stringify({
|
||||
settings: TEST_SETTINGS_DEFAULT_THEME,
|
||||
}),
|
||||
}
|
||||
)
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Selectors and constants
|
||||
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
|
||||
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
|
||||
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
|
||||
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
|
||||
const streamBackgroundPixelIsColor = async (
|
||||
color: [number, number, number]
|
||||
) => {
|
||||
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
|
||||
}
|
||||
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
|
||||
|
||||
await test.step(`Test setup`, async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await expect(toolbar).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Check the background color is light before`, async () => {
|
||||
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
|
||||
await expect
|
||||
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
|
||||
await page.emulateMedia({ colorScheme: 'dark' })
|
||||
})
|
||||
|
||||
await test.step(`Check the background color is dark after`, async () => {
|
||||
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
|
||||
await expect
|
||||
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
|
||||
page,
|
||||
}) => {
|
||||
|
@ -8,7 +8,7 @@ import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
getThemeColorForEngine,
|
||||
darkModeMatcher,
|
||||
getOppositeTheme,
|
||||
setThemeClass,
|
||||
Themes,
|
||||
@ -34,6 +34,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { saveSettings } from 'lib/settings/settingsUtils'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -113,26 +114,9 @@ export const SettingsAuthProviderBase = ({
|
||||
sceneInfra.baseUnit = newBaseUnit
|
||||
},
|
||||
setEngineTheme: ({ context }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_background_color',
|
||||
color: getThemeColorForEngine(context.app.theme.current),
|
||||
},
|
||||
})
|
||||
|
||||
const opposingTheme = getOppositeTheme(context.app.theme.current)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_default_system_properties',
|
||||
color: getThemeColorForEngine(opposingTheme),
|
||||
},
|
||||
})
|
||||
engineCommandManager
|
||||
.setTheme(context.app.theme.current)
|
||||
.catch(reportRejection)
|
||||
},
|
||||
setEngineScaleGridVisibility: ({ context }) => {
|
||||
engineCommandManager.setScaleGridVisibility(
|
||||
@ -261,14 +245,13 @@ export const SettingsAuthProviderBase = ({
|
||||
// because there doesn't seem to be a good way to listen to
|
||||
// events outside of the machine that also depend on the machine's context
|
||||
useEffect(() => {
|
||||
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
if (settingsState.context.app.theme.current !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
|
||||
matcher.addEventListener('change', listener)
|
||||
return () => matcher.removeEventListener('change', listener)
|
||||
darkModeMatcher?.addEventListener('change', listener)
|
||||
return () => darkModeMatcher?.removeEventListener('change', listener)
|
||||
}, [settingsState.context])
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,12 @@ import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { exportSave } from 'lib/exportSave'
|
||||
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import {
|
||||
Themes,
|
||||
getThemeColorForEngine,
|
||||
getOppositeTheme,
|
||||
darkModeMatcher,
|
||||
} from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import {
|
||||
ArtifactGraph,
|
||||
@ -1393,6 +1398,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
|
||||
private onEngineConnectionOpened = () => {}
|
||||
private onEngineConnectionClosed = () => {}
|
||||
private onDarkThemeMediaQueryChange = (e: MediaQueryListEvent) => {
|
||||
this.setTheme(e.matches ? Themes.Dark : Themes.Light).catch(reportRejection)
|
||||
}
|
||||
private onEngineConnectionStarted = ({ detail: engineConnection }: any) => {}
|
||||
private onEngineConnectionNewTrack = ({
|
||||
detail,
|
||||
@ -1479,30 +1487,13 @@ export class EngineCommandManager extends EventTarget {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.onEngineConnectionOpened = async () => {
|
||||
// Set the stream background color
|
||||
// This takes RGBA values from 0-1
|
||||
// So we convert from the conventional 0-255 found in Figma
|
||||
|
||||
void this.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'set_background_color',
|
||||
color: getThemeColorForEngine(this.settings.theme),
|
||||
},
|
||||
})
|
||||
|
||||
// Sets the default line colors
|
||||
const opposingTheme = getOppositeTheme(this.settings.theme)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_default_system_properties',
|
||||
color: getThemeColorForEngine(opposingTheme),
|
||||
},
|
||||
})
|
||||
// Set the theme
|
||||
this.setTheme(this.settings.theme).catch(reportRejection)
|
||||
// Set up a listener for the dark theme media query
|
||||
darkModeMatcher?.addEventListener(
|
||||
'change',
|
||||
this.onDarkThemeMediaQueryChange
|
||||
)
|
||||
|
||||
// Set the edge lines visibility
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@ -1793,6 +1784,10 @@ export class EngineCommandManager extends EventTarget {
|
||||
EngineConnectionEvents.NewTrack,
|
||||
this.onEngineConnectionNewTrack as EventListener
|
||||
)
|
||||
darkModeMatcher?.removeEventListener(
|
||||
'change',
|
||||
this.onDarkThemeMediaQueryChange
|
||||
)
|
||||
|
||||
this.engineConnection?.tearDown(opts)
|
||||
|
||||
@ -2155,6 +2150,34 @@ export class EngineCommandManager extends EventTarget {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the engine's theme
|
||||
*/
|
||||
async setTheme(theme: Themes) {
|
||||
// Set the stream background color
|
||||
// This takes RGBA values from 0-1
|
||||
// So we convert from the conventional 0-255 found in Figma
|
||||
this.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_background_color',
|
||||
color: getThemeColorForEngine(theme),
|
||||
},
|
||||
}).catch(reportRejection)
|
||||
|
||||
// Sets the default line colors
|
||||
const opposingTheme = getOppositeTheme(theme)
|
||||
this.sendSceneCommand({
|
||||
cmd_id: uuidv4(),
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_default_system_properties',
|
||||
color: getThemeColorForEngine(opposingTheme),
|
||||
},
|
||||
}).catch(reportRejection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the visibility of the scale grid in the engine scene.
|
||||
* @param visible - whether to show or hide the scale grid
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { AppTheme } from 'wasm-lib/kcl/bindings/AppTheme'
|
||||
|
||||
/** A media query matcher for dark mode */
|
||||
export const darkModeMatcher =
|
||||
(typeof globalThis.window !== 'undefined' &&
|
||||
'matchMedia' in globalThis.window &&
|
||||
globalThis.window.matchMedia('(prefers-color-scheme: dark)')) ||
|
||||
undefined
|
||||
|
||||
export enum Themes {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
@ -25,7 +32,7 @@ export function appThemeToTheme(
|
||||
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
||||
return typeof globalThis.window !== 'undefined' &&
|
||||
'matchMedia' in globalThis.window
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? darkModeMatcher?.matches
|
||||
? Themes.Dark
|
||||
: Themes.Light
|
||||
: Themes.Light
|
||||
|
Reference in New Issue
Block a user