Theme engine architecture
This page documents the runtime theme engine in the Carbon desktop app. It pairs with Authoring (how to ship a pack) and theme.yaml reference (field-by-field spec).
End-to-end pipeline
Section titled “End-to-end pipeline”App startup / Settings → ThemeManager.loadTheme(themeId) │ ├─ Resolve id (built-in list + disk merge; built-in wins on id clash) ├─ Disk only: ThemesApi.ReadStylesheet / ReadJavascript ├─ compileDiskThemeStylesheet() (Dart Sass or passthrough .css) ├─ applyThemeStyle() → #carbon-custom-theme, data-theme, carbon:theme-changed ├─ applyThemeIcons() or resetThemeIcons() ├─ Tauri set_backdrop (unless JS enabled + layer === backdrop) └─ themeHostService.syncJavascriptTheme() → one sandbox iframe (srcdoc)Only one JavaScript theme iframe runs at a time. Selecting another theme sends destroy, tears down timers, and mounts the new bundle.
Built-in vs disk themes
Section titled “Built-in vs disk themes”| Built-in | Disk (Skeleton/Themes/<id>/) |
|
|---|---|---|
| Registration | defaultThemes in core/src/scripts/theme.ts |
ThemeManager.DiscoverAll() |
| Stylesheet | Vite ?inline at app build |
configuration.source via ThemesApi.ReadStylesheet |
| JavaScript | dist/theme.js via ?raw import |
javascript.entry via ThemesApi.ReadJavascript |
| Manifest on disk | N/A (embedded in bundle) | theme.yaml or manifest.json |
| Id collision | Wins | Skipped if id matches a built-in |
Folder name = theme id. Carbon creates Skeleton/Themes at runtime if missing.
Persistence
Section titled “Persistence”| Store | Key / API | Notes |
|---|---|---|
| Backend | AppSettings.GetStyle() / SetStyle(themeId) |
When XPUI backend is ready |
| Browser | localStorage carbon-theme-id |
Fallback |
| Migration | carbon-canvas → slipstream |
Legacy id |
| Unknown id | Falls back to default-dark |
On load |
Startup resync
Section titled “Startup resync”ThemeManager.init() merges disk themes, loads the saved id, and calls themeHostService.ensureInitialized(). If CSS was applied before the bridge listened, resyncJavascriptTheme() re-mounts JS for the active theme.
Backend (project/carbon/)
Section titled “Backend (project/carbon/)”| Type | Role |
|---|---|
ThemeManifestFiles |
Resolves theme.yaml vs manifest.json |
ThemeYamlParser |
Parses YAML/JSON into ThemeManifest |
ThemeManifestSchema |
Allow-lists fields, validates javascript.layer, safe paths, 256 KiB JS cap |
ThemeManager |
Scans directories, reads stylesheet / JS / assets |
ThemesApi (XPUI) |
ListDiskThemes, ReadStylesheet, ReadJavascript, ReadAsset |
Disk manifest validation (JSON)
Section titled “Disk manifest validation (JSON)”Allowed root keys: manifestVersion, metadata, configuration, javascript.
javascript.layer must be backdrop or overlay when set.
C# javascript fields persisted from disk: enabled, entry, layer only. The desktop UI also reads interactive from bundled manifest.json in the repo; that flag is not in ThemeJavascriptSettings yet, so YAML-only disk themes cannot rely on interactive round-tripping through the backend.
configuration.platformFallback is allowed in JSON schema validation but is not applied by the current loader, treat as reserved.
Path and size rules
Section titled “Path and size rules”configuration.sourceandjavascript.entrymust be relative, no.., must stay inside the theme directory.- JavaScript reads enforce 256 KiB (
ThemeManifestSchema.MaxJavascriptBytes).
Frontend stylesheet pipeline
Section titled “Frontend stylesheet pipeline”ThemeManager.applyThemeStyle(css): writes<style id="carbon-custom-theme">and re-appends it to<head>so--crbn-*overrides bundled defaults.document.documentElement.dataset.theme: active theme id.windoweventcarbon:theme-changed:{ detail: { themeId } }.- Disk SCSS:
sass.compileStringinthemeDiskStyles.tson the single file returned by the backend (no@importresolution for disk partials). - Preview colors: manifest
configuration.colorsor regex extraction from CSS increateTheme().
Windows Mica / backdrop
Section titled “Windows Mica / backdrop”On Tauri, invoke('set_backdrop', { class, theme }) runs after two requestAnimationFrame callbacks so the transparent webview has painted.
| Condition | Mica |
|---|---|
javascript.enabled && layer === 'backdrop' |
Skipped (canvas visible behind glass) |
javascript.enabled && layer === 'overlay' |
Can still apply per configuration.backdrop |
window.__carbonDeferBackdrop set during shell boot |
Deferred until activateShellBackdrop() |
Backdrop type values are normalized (mica-alt → mica_alt) before the Tauri call.
Mock / web-only builds
Section titled “Mock / web-only builds”IS_MOCK skips the JS host. Disk themes require the real backend (loadDiskThemeStylesheet throws if backend is not ready).
JavaScript host (themeHostService.ts)
Section titled “JavaScript host (themeHostService.ts)”#carbon-theme-host:position: fixed; inset: 0; pointer-events: none; z-index: 0, first child of#app(below.app-container).- Iframe:
sandbox="allow-scripts"only (noallow-same-origin),credentialless,referrerpolicy="no-referrer",srcdocfrombuildThemeShellHtml().
Layer stacking
Section titled “Layer stacking”javascript.layer |
Iframe z-index | Colloquial name |
|---|---|---|
backdrop (default) |
0 |
Behind UI |
overlay |
9000 |
“Frontdrop” / in front of shell |
Lifecycle and watchdog
Section titled “Lifecycle and watchdog”| Constant | Value | On failure |
|---|---|---|
READY_TIMEOUT_MS |
5 s | Teardown + toast “failed to start” |
WATCHDOG_INTERVAL_MS |
15 s | Ping iframe |
PING_TIMEOUT_MS |
2 s | Two missed pongs → teardown + toast |
DESTROY_GRACE_MS |
500 ms | Delay before removing iframe DOM |
Security: a second load on the iframe (navigation) tears down the theme and shows a security toast.
Interactive pointer bridge
Section titled “Interactive pointer bridge”When javascript.interactive is true on the frontend manifest:
- Host listens for
pointerdown/move/up/cancelonwindow. - Forwards
appevents:{ kind: 'pointer', phase, x, y, buttons, pressure }. - Iframe stays
pointer-events: noneso the app remains clickable.
Host → theme app events
Section titled “Host → theme app events”| Payload | When |
|---|---|
{ type: 'theme-activated', themeId } |
After iframe ready and init |
{ type: 'theme-deactivated', themeId } |
Before teardown on theme switch |
| Pointer object | When interactive: true |
Iframe shell (themeBootstrap.ts)
Section titled “Iframe shell (themeBootstrap.ts)”buildThemeShellHtml(themeSource, themeTokensCss):
- Runs
validateThemeJavascriptagain at shell build time. - Injects CSP (no
connect-src, no workers, no nestedframe-src, inline scripts only). - Injects wrapper (
CarbonTheme+ internalpostMessagebridge), then your bundle.
The wrapper creates ctx.canvas and ctx.root, wires host message types to ctx.events, and posts ready after CarbonTheme.mount runs.
Optional themeTokensCss: snapshot of --crbn-* on :root injected as <style id="carbon-theme-tokens"> inside the iframe for first-frame token reads.
Bridge (themeBridgeService.ts)
Section titled “Bridge (themeBridgeService.ts)”| Direction | source field |
Allowed type values |
|---|---|---|
| Host → iframe | carbon-theme-host |
init, resize, pause, resume, destroy, tokens, event, ping |
| Iframe → host | carbon-theme |
ready, pong only |
Origin must be opaque sandbox ("null"). Theme source must not call postMessage; the static guard blocks it in your bundle.
init payload (typical)
Section titled “init payload (typical)”settings:width,height,dpr,reducedMotion,paused,seedseed: also drivesctx.random(mulberry32)tokens: all computed--crbn-*from the live shell
resize updates settings and resizes the default 2D canvas with a DPR transform.
Static guard (themeJavascriptGuard.ts)
Section titled “Static guard (themeJavascriptGuard.ts)”Runs when registering built-ins, loading disk JS, and building srcdoc.
- Max 256 KiB
- Single entry:
CarbonTheme.mount(function (ctx) { ... }); - Forbidden APIs after comment/string strip: full table in reference
Failure → toast “JavaScript theme blocked for security” or “failed to start”; no iframe.
Source file map
Section titled “Source file map”| File | Responsibility |
|---|---|
core/src/scripts/theme.ts |
Registry, ThemeManager, Mica, CSS inject, disk merge |
core/src/scripts/themeDiskStyles.ts |
API reads + Sass compile |
core/src/scripts/themeBootstrap.ts |
Iframe HTML, CSP, CarbonTheme wrapper, layer z-index |
core/src/scripts/themeJavascriptGuard.ts |
Static validation |
core/src/services/themeHostService.ts |
Mount/teardown, watchdog, pointer bridge |
core/src/services/themeBridgeService.ts |
postMessage routing |
core/src/stores/icons.ts |
applyThemeIcons / resetThemeIcons |
carbon/ThemeManager.cs |
Disk discovery and file reads |
Themes documentation (sidebar)
Section titled “Themes documentation (sidebar)”| Page | Link |
|---|---|
| Overview | Themes |
| Authoring | Create a theme pack |
| Engine architecture | This page |
| Manifest reference | theme.yaml |
| Built-in catalog | Shipped themes |