27 KiB
Theme migration plan: full migration to the Zeron theme model
Full migration of crates/theme (currently derived from zed-industries/zed crates/theme) to the architecture of zeronsh/zeron crates/theme.
Scope decision (v2): wholesale migration. We do not preserve the current brand look, do not keep the old ~70-role UI color vocabulary, and do not bridge the old JSON theme assets. Zeron's model, role set, builtins, and defaults become coop's. A branding refresh (custom coop family/seeds) happens later, on top of the new model.
- Coop license: GPLv3 (theme crate currently credits Zed).
- Zeron license: MIT — code may be ported into coop with attribution (add zeron to
crates/theme/README.mdcredits, record the ported revision).
1. Current state (coop)
Crate layout (crates/theme)
| File | Contents |
|---|---|
src/lib.rs |
Theme gpui Global (colors + mode + font/radius/shadow/scrollbar/notification), ActiveTheme trait, init(), sync_base() projection into gpui-base tokens, layout constants (TITLEBAR_HEIGHT, …) |
src/theme.rs |
ThemeMode (Light/Dark), ThemeFamily { id, name, author, url, light: ThemeColors, dark: ThemeColors }, from_file/from_assets JSON loading, tests |
src/colors.rs |
ThemeColors: ~70 Hsla roles (Zed-style: element_*, secondary_*, danger_*, warning_*, ghost_element_*, tab_*, scrollbar_*, …) with light()/dark() defaults built from color scales |
src/scale.rs |
Zed ColorScale/ColorScaleStep/ColorScaleSet (12-step scales) |
src/registry.rs |
ThemeRegistry: gpui Entity global; loads themes/** from the AssetSource into HashMap<path, Rc<ThemeFamily>> |
src/geometry.rs, src/notification.rs, src/scrollbar_mode.rs, src/platform_kind.rs |
App chrome: Edges/Side/AxisExt, NotificationSettings, ScrollbarMode, PlatformKind |
Assets & consumers
assets/themes/*.json— 8 theme family files (Zed family format):aurora,catppuccin-{latte,frappe,macchiato,mocha},flexoki,forest,ocean; embedded viacrates/assets(rust-embed).- Apps init:
desktop/src/main.rsandweb/src/lib.rscallui::init(cx)thentheme::init(cx). - Settings (
crates/settings/src/lib.rs): persiststheme: Option<String>(asset path) +theme_mode: ThemeModetoconfig_dir()/.settings(JSON viasmol::fs, wasm-gated);AppSettings::apply_themeloadsThemeFamily::from_assets(path)→Theme::apply_theme+Theme::change. - Theme picker:
crates/workspace/src/lib.rstheme_selector()listsThemeRegistryentries (name, author, url link). - Preferences:
crates/workspace/src/dialogs/settings.rs— Light/Dark toggle viaTheme::change. sync_base()mirrors coop colors into thegpui-baseglobal theme (focus ring, selection, scrollbars, overlays, typography).
Usage scale (defines the rewrite scope)
use theme::ActiveThemeacross 7 crates.- ~370
cx.theme().*references in 51 files:ui(23),workspace(22),chat_ui(1, one largelib.rs),nip29_ui(4),nip4e(1). ~305 are color roles; the rest areradius(41),shadow(10),radius_lg(8),notification(2),font_*(2) — these app-chrome fields are unchanged by the migration. - 55 distinct color roles are referenced;
ThemeColorsis never named outsidecrates/theme.
2. Target state (zeron crates/theme)
A source-neutral, gpui-free domain model plus an offline import pipeline:
| File | Contents |
|---|---|
src/lib.rs |
Domain model: Appearance, Color (rgba u8, hex serde, contrast/mix/blend_over/ensure_contrast/best_on_color), SurfaceTreatment/SurfacePreference, AccentPreset/AccentSelection/AccentRoles (primary/strong/wash/on/selection/caret/activity/glyph[3], derived with WCAG hardening), ThemeSelection { light, dark }, ThemeSource (provenance + sha256 asset_hash), ThemeColors (28 roles: background, shell, raised, card, dialog, overlay, hover, active, border, border_strong, text, text_muted, text_faint, solid, on_solid, danger(+_muted), warning(+_muted), success(+_muted), input, cursor, diff_add/delete/hunk), TerminalPalette (bg/fg/selection + ansi[16]), ThemeVariant (id, family_id, name, appearance, recommended_surface_treatment, colors, accent, syntax map, terminal, source), ThemeFamily { id, name, variants }, ThemeRegistry (builtin()/active()/variant()/variants_for()/resolve()/validate()), ValidationIssue/Severity/Category, VisualFixture, replace_custom_families() |
src/builtins.rs |
19 families / 30 variants compiled in Rust from a Seeds struct; derived roles (border/hover/active alphas, dialog/overlay mixes, accent roles, 24-key syntax map, terminal palette); sha256 provenance of the resolved definition |
src/library.rs |
CustomThemeLibrary persisted as theme-library.json (16 MB / 256 entries / 1024 variants caps, atomic recoverable saves, backup recovery); sources: ImportedSnapshot, LinkedFile, LinkedPackage, EditableFile; install/reload/unlink/duplicate/remove with last-known-good semantics |
src/vscode.rs |
Offline VS Code import: JSONC (json5) + include resolution + external token files + TextMate .tmTheme (plist); workbench-key → zeron-role mapping tables; contrast hardening; ImportReport; extension-package compilation with per-variant failure isolation; security bounds (4 MB sources, include depth 32, ≤128 variants, path containment) |
src/bin/zeron-theme-import.rs |
CLI wrapper around compile_source |
Dependencies: anyhow, serde, serde_json, json5, plist, sha2, thiserror (clap only for the CLI). No gpui.
3. Key architectural differences
| Aspect | Coop today (Zed-style) | Zeron target |
|---|---|---|
| Color type | gpui::Hsla everywhere |
Own Color (rgba u8) with WCAG contrast math; converted to Hsla only at the gpui boundary |
| Family shape | { light: ThemeColors, dark: ThemeColors } pair |
variants: Vec<ThemeVariant>, each variant carries its own appearance + full payload |
| Role count | ~70 UI roles | 28 semantic roles + derived AccentRoles |
| Accent | Implicit (element_background) |
First-class: presets, per-variant AccentRoles, runtime AccentSelection override |
| Builtins | JSON files in assets/themes, loaded via AssetSource at runtime |
Compiled into Rust (builtins.rs), sha256 provenance |
| Custom themes | None | Durable library (snapshot/link/editable) + VS Code import with reports & hardening |
| Validation | None | validate(): contrast thresholds, unique ids, provenance completeness; blocking vs. reviewable |
| Registry | gpui Entity global over a path map |
Plain struct; builtin() static + process-wide custom families (RwLock) |
| Syntax / terminal | Not modeled | syntax map (24 keys) + TerminalPalette per variant |
| Surfaces | N/A | SurfaceTreatment (Opaque/Frosted) recommendation + user SurfacePreference |
4. Guiding decisions
- D1 — Two layers, one crate.
crates/themegets a gpui-free domain module (port of zeron's model, kept faithful so upstream diffs stay readable) and a gpui runtime module (coop'sThemeglobal,ActiveTheme,sync_base, app chrome). No new crate. - D2 — Zeron's role vocabulary is the only UI color vocabulary. The old ~70-role
ThemeColorsis deleted. UI code uses the 28 zeron roles +AccentRolesexclusively. All 305 color call sites are rewritten (Phase 3) using the mapping in §7. - D3 — Zeron builtins and defaults adopted as-is. Port all 19 families / 30 variants unchanged. Default selection:
ThemeSelection::default()=zeron-light/zeron-dark.assets/themes/*.jsonand the asset-loading registry are deleted. A coop-branded family is a later, separate change (swap seeds, keep the model). - D4 — Runtime exposes a resolved
Hslasnapshot. The gpuiThemeglobal storesvariant: Arc<ThemeVariant>, resolvedappearance,accent: AccentRoles,surface: SurfaceTreatment, and a precomputed snapshot with the same field names as the domainThemeColors/AccentRolesbut typedHsla(ResolvedColors,ResolvedAccentRoles). Call sites stay ergonomic (cx.theme().shell,cx.theme().accent.primary) and gpui-native (.alpha(...)still works). App-chrome fields (font_family,font_size,radius,radius_lg,shadow,scrollbar_mode,notification) remain onTheme, unchanged. - D5 — Settings.
theme: Option<String>+theme_mode: ThemeMode→theme_selection: ThemeSelection+ appearance setting; old keys are dropped on load (theme_modemaps trivially toAppearance; the old theme path is reset to defaults — the ids changed anyway). New settings:accent_selection: AccentSelection,surface_preference: SurfacePreference. Startup keeps following the OS appearance viasync_system_appearance. - D6 — Transitional compile scaffold, not an end state. To keep every commit building while 51 files are rewritten, Phase 2 temporarily provides the removed role names (
ghost_element_hover,element_background,tab_*, …) as fields on the resolved snapshot, computed from zeron roles per §7. They carry a doc comment "transitional — delete in Phase 5" and are removed as soon as call-site migration completes. This is a tactic, not a compat layer: no visual parity is promised. - D7 — Scope cuts for v1. No frosted rendering (enums land; resolution returns
Opaque; gpui blur later).TerminalPalette/syntaxare carried in the model with no consumer yet (future: code-block highlighting).VisualFixtureis not ported. The import CLI (bin/) is not ported. - D8 — Web target.
library.rs/vscode.rsusestd::fs; gate them behind#[cfg(not(target_arch = "wasm32"))](or a cargo feature) socoop_webkeeps building, mirroring howsettingsgates file IO.
5. Target crate layout
crates/theme/
├── Cargo.toml # + thiserror, sha2 (json5/plist with Phase 6); − schemars
└── src/
├── lib.rs # re-exports; layout constants; init(); sync_base(); ActiveTheme (runtime)
├── model.rs # zeron lib.rs port: Color, Appearance, Surface*, Accent*,
│ # ThemeSelection, ThemeSource, ThemeColors, TerminalPalette,
│ # ThemeVariant, ThemeFamily, ThemeRegistry, ValidationIssue (+ tests)
├── builtins.rs # zeron builtins port: Seeds machinery + 19 families (+ tests)
├── runtime.rs # Theme gpui global over the domain: resolve, change, apply_variant,
│ # sync_system_appearance, sync_scrollbar_appearance
├── resolved.rs # ResolvedColors / ResolvedAccentRoles (Hsla mirror of the domain
│ # structs) + Color→Hsla conversion + transitional scaffold fields
├── geometry.rs # unchanged (Edges/Side/AxisExt; used by NotificationSettings)
├── notification.rs # unchanged
├── scrollbar_mode.rs # unchanged
├── platform_kind.rs # unchanged (font selection)
├── library.rs # Phase 6 (optional): CustomThemeLibrary
└── vscode.rs # Phase 6 (optional): VS Code import
Deleted: src/colors.rs, src/scale.rs, src/theme.rs, src/registry.rs, assets/themes/*, the themes include in crates/assets, and the schemars dependency.
6. Implementation phases
Each phase leaves the workspace building and tested (D6 makes this possible during the rewrite). Commit boundaries follow phases.
Phase 0 — Prep (S)
- Record the ported zeron revision (commit hash) for future diffing; add MIT attribution to
crates/theme/README.md. - Baseline
cargo test/cargo buildstate; confirm D7 scope cuts. - Create the migration branch.
Acceptance: branch exists; decisions recorded.
Phase 1 — Port the zeron domain model (M)
crates/theme/Cargo.toml:+ thiserror,+ sha2(both workspace or pinned).- Create
src/model.rs— faithful port of zeron'slib.rs(§2), keeping its why doc comments (provenance hashing, severity semantics, …) and its test module (color round-trip, accent contrast, surface resolution, builtin validation/counts). - Create
src/builtins.rs— faithful port of theSeedsmachinery and all 19 families (derived roles, syntax map, ANSI palettes, sha256 self-hash), including the registry-count tests. - Export from
lib.rswithout wiring anything into the app yet. Project rules:Resultpropagation, nounwrap();expectonly for constant builtin seed colors (statically covered by the builtin validation test, as upstream).
Acceptance: cargo test -p theme green with ported tests; ThemeRegistry::builtin().validate() reports no blocking issues; app behavior unchanged.
Phase 2 — Runtime rework (L)
- Add
src/resolved.rs:ResolvedColors(28 fields,Hsla) andResolvedAccentRoles(primary/strong/wash/on/selection/caret/activity/glyph,Hsla), built from aThemeVariant+ resolvedAccentRolesat theme-change time (cheap, precomputed). Conversion: zeronColor { r, g, b, a }→gpui::Rgba→Hsla. - Add
src/runtime.rsand rewritelib.rsaround it:Themeglobal fields:variant: Arc<ThemeVariant>,appearance: Appearance,accent: AccentRoles,surface: SurfaceTreatment,colors: ResolvedColors(plusDeref<Target = ResolvedColors>), and the unchanged app-chrome fields;Theme::change(appearance): resolveThemeRegistry::active().resolve(&selection, appearance)→ rebuild the snapshot; still falls back to zeron defaults;Theme::apply_variant(variant)replacesapply_theme(family);sync_system_appearance/sync_scrollbar_appearancekeep their roles;ActiveThemeunchanged;ThemeModedeleted, replaced byAppearance(From<WindowAppearance>ported).
- Add the transitional scaffold fields to
ResolvedColors(D6): the removed role names computed from zeron roles per §7, e.g.surface_background = shell,ghost_element_hover = hover,element_background = accent.primary. All 55 used roles covered so downstream compiles untouched. - Delete
src/colors.rs,src/scale.rs,src/theme.rs,src/registry.rs; deleteassets/themes/*.jsonand thethemesinclude incrates/assets/src/lib.rs; removeschemarsfromCargo.toml. - Settings (
crates/settings/src/lib.rs): swap persisted fields totheme_selection(+#[serde(default)]accent_selection,surface_preference), map oldtheme_mode→ appearance on load, drop the oldthemekey;apply_themeresolves through the registry (Theme::change). Updatedesktop/webinit if signatures moved. - Rewrite
sync_base()per §9.
Acceptance: app builds and runs on zeron defaults; appearance toggle + OS sync work; settings round-trip and the old .settings file loads (theme resets to defaults, mode preserved); no downstream crate has been touched yet.
Phase 3 — Call-site migration: 51 files, 5 crates (L)
Migrate in dependency order; after each crate the workspace builds and that crate uses only zeron vocabulary:
ui(23 files) — includes the component redesigns of §8 (button.rsvariants,tab/,input/,menu/,dock/,root.rs,title_bar.rs, …).workspace(22 files) — sidebar/panels/dialogs +theme_selectorplaceholder compiles via scaffold.chat_ui(1 large file),nip29_ui(4),nip4e(1).- Mechanics: apply the §7 mapping mechanically first, then review each file for roles that encode intent (e.g.
secondary_*used as chip styling →accent.washvsraised) and for contrast-sensitive spots (text on tinted backgrounds →ensure_contrastvia the resolved accent where needed).
Acceptance: grep -rE 'theme\(\)\.(element_|secondary_|ghost_element_|tab_|scrollbar_thumb|title_bar|panel_background|surface_background|elevated_surface)' crates returns nothing; each crate compiles + its tests pass before moving on.
Phase 4 — Theme UI renewal (M)
theme_selector(): listThemeRegistry::active().variants_for(appearance)grouped by family; show variant name, family, provenance (url/license); selecting writestheme_selection.set_variant(appearance, id)and applies.- Preferences dialog (
workspace/dialogs/settings.rs): Light/Dark toggle nowAppearance; add Accent picker (AccentSelection: Theme default +AccentPreset::ALL) and Surface preference; changing accent re-resolvesAccentRolesand refreshes windows. - Register validation feedback (non-blocking
ValidationIssues) somewhere visible later — minimal v1: log only.
Acceptance: picker lists families/variants filtered by appearance; selection persists per appearance; accent override recomputes accent-derived UI live; surface preference stored (rendering effect deferred per D7).
Phase 5 — Remove the scaffold & dead code (S)
- Delete every transitional field from
ResolvedColorsand the §7 scaffold computations. - Remove leftover APIs (
from_assets, old tests/docs references), runcargo clippy+cargo testworkspace-wide. - Update
crates/theme/README.md(Zed + Zeron credits, ported revision).
Acceptance: no scaffold remains; cargo build (desktop) and cargo build -p coop_web both green.
Phase 6 — Custom theme library + VS Code import (L, optional)
- Port
src/vscode.rs(import pipeline, mapping tables, hardening, reports, package compilation, security bounds) andsrc/library.rs(durable library, atomic recoverable saves, last-known-good reload, duplicate-as-editable). Addjson5+plistdeps. - Gate file IO for the web target (D8); library lives under
common::config_dir()(theme-library.json,custom-themes/*.zeron-theme.json). - UI: "Import theme…" flow (file picker →
compile_source→ report dialog with mappings/adjustments/validation) and a library manager (reload/unlink/duplicate/remove). Errors surface in the UI per project rules. - After library load/mutation:
install_runtime()→ re-resolve the active theme.
Acceptance: ported zeron test suites pass; importing a VS Code package yields usable variants with a report; broken sources keep the last known good theme; coop_web unaffected by the gated IO.
Phase 7 — Validation, tests, release (M)
- Add a runtime smoke test: resolving every builtin variant produces a
ResolvedColorssnapshot with finite/valid values, andTheme::changeround-trips appearances. - Settings migration test (old
.settings→ new fields). - Full QA pass on desktop (+ web if Phase 6 landed): §11 checklist; spot-check a handful of variants (zeron light/dark, catppuccin mocha/latte, github dark).
- Release notes; schedule the deferred branding pass (custom coop family seeds) as follow-up work.
7. Call-site rewrite mapping (old role → zeron vocabulary)
accent.* refers to the resolved AccentRoles. Where intent matters more than mechanics, prefer the Intent column over the mechanical mapping.
| Old role | New (mechanical) | Intent / notes |
|---|---|---|
background |
background |
|
surface_background |
shell |
sidebars, secondary surfaces |
elevated_surface_background |
raised |
hover cards, popovers, lists |
panel_background |
shell |
|
overlay |
overlay |
scrim/backdrop (domain role is a surface; blend as needed) |
title_bar |
shell |
|
window_border |
border_strong |
|
border |
border |
|
border_variant |
border (optionally .opacity(...)) |
|
border_focused / border_selected |
accent.primary |
|
border_transparent |
gpui::transparent_black() |
|
border_disabled |
border low alpha |
|
ring |
accent.primary |
focus ring (also via sync_base) |
text |
text |
|
text_muted |
text_muted |
|
text_placeholder |
text_faint |
|
text_accent |
accent.primary |
|
text_danger |
danger |
|
text_warning |
warning |
|
icon |
text_muted |
|
icon_muted |
text_faint |
|
icon_accent |
accent.primary |
|
element_background |
accent.primary (emphasis) / solid (neutral) |
§8 button redesign |
element_foreground |
accent.on / on_solid |
|
element_hover |
mix(accent.primary, background, ~0.12) |
precompute on ResolvedAccentRoles if reused |
element_active / element_selected |
mix(accent.primary, background, ~0.2/0.28) |
|
element_disabled |
accent.primary.opacity(0.3) |
|
secondary_background |
accent.wash |
tinted chips/selections |
secondary_foreground |
contrast-safe accent text (precomputed) | e.g. accent.primary.ensure_contrast(wash, 4.5) at resolve time |
secondary_hover / secondary_active / secondary_selected |
accent.selection / deeper wash alphas |
|
secondary_disabled |
accent.wash.opacity(0.5) |
|
danger_background |
danger_muted (tint) or danger.opacity(0.15) |
|
danger_foreground |
danger (ensure contrast on tint) |
|
danger_hover/active/selected |
danger.opacity(...) steps |
|
danger_disabled |
danger.opacity(0.12) |
|
warning_* |
same pattern from warning / warning_muted |
|
ghost_element_background |
gpui::transparent_black() |
|
ghost_element_background_alt |
raised (or hover) |
|
ghost_element_hover |
hover |
|
ghost_element_active |
active |
|
ghost_element_selected |
active |
|
ghost_element_disabled |
border.opacity(0.4) |
|
tab_background |
shell |
|
tab_foreground |
text_muted |
|
tab_hover_background |
hover |
|
tab_active_background |
background |
active tab merges into the surface |
tab_active_foreground |
text |
|
scrollbar_thumb_background |
border.opacity(0.7) |
scrollbar painting mostly via gpui-base |
scrollbar_thumb_hover_background |
border_strong |
|
scrollbar_track_background/border |
transparent / border |
|
drop_target_background |
accent.selection |
|
cursor |
cursor |
|
selection |
accent.selection |
|
radius, radius_lg, shadow, font_family, font_size, notification, scrollbar_mode |
unchanged | app chrome on Theme |
Roles with no consumer after migration (success, diff_*, terminal, syntax) remain domain-only, available for future features.
8. Component redesign notes (ui crate, Phase 3.1)
Buttonvariants (button.rs):Primary→accent.primarybg,accent.onfg, hover = mix toward background (precomputed onResolvedAccentRolesasprimary_hover/primary_active).Secondary→accent.washbg, contrast-safe accent fg.Danger→dangerbg,danger.best_on_color()fg (precompute); tinted variant usesdanger_muted.Warning→warningbg + best-on fg /warning_mutedtint.Ghost { alt }→ transparent /raisedbg;hover/activestates from domain roles.Transparent→text_faintfg.
- Tabs (
tab/): inactive onshellwithtext_muted; hoverhover; active merges tobackgroundwithtext; accent indicator viaaccent.primary. - Inputs (
input/): bginput, borderborder, focus ringaccent.primary, placeholdertext_faint. - Precomputed derived fields that several components need (
primary_hover,primary_active,accent_text_on_wash,onDanger…) belong onResolvedAccentRoles— computed once at resolve time withensure_contrast, not ad-hoc at call sites.
9. sync_base() mapping (new)
gpui-base token |
Source |
|---|---|
appearance |
theme.appearance |
tokens.background / foreground |
background / text |
tokens.surface / surface_foreground |
shell / text |
tokens.primary / primary_foreground |
accent.primary / accent.on |
tokens.secondary / secondary_foreground |
raised / text |
tokens.muted / muted_foreground |
hover / text_muted |
tokens.accent / accent_foreground |
accent.wash / text |
tokens.destructive / destructive_foreground |
danger / precomputed on-danger |
tokens.border / input |
border / input |
tokens.ring |
accent.primary |
tokens.selection |
accent.selection |
scrollbar.mode |
scrollbar_mode (unchanged) |
typography.sans |
font_family (unchanged) |
10. Settings migration
- Old
.settings:{ "theme": "themes/<name>.json" | null, "theme_mode": "Light"|"Dark", … }. - New:
theme_selection: ThemeSelection(defaultzeron-light/zeron-dark), appearance setting,accent_selection,surface_preference(all#[serde(default)]). - On load:
theme_modemaps toAppearance; the oldthemepath is ignored (ids changed; optional nicety: matchcatppuccin-mocha-style family names to builtin ids). Old keys are dropped on next save.
11. Dependency changes
| Crate | Change |
|---|---|
crates/theme |
+ thiserror, + sha2, + json5, + plist (Phase 6); − schemars; keep gpui, gpui-base, anyhow, log, serde, serde_json |
crates/assets |
remove the themes/**/* include |
| others | none |
12. Risks & open questions
- Call-site drift: mechanical mapping can flatten intent (e.g. everything "muted" becoming
text_faint). Mitigated by the per-file review pass in Phase 3.4 and precomputed contrast-safe fields (§8). - Big-bang compile risk: contained by the D6 scaffold — every intermediate commit builds.
- Visual QA: defaults change to zeron's look; accepted by scope. Spot-check a handful of variants; full polish belongs to the later branding pass.
- Frosted surfaces: enums land now, rendering later; the preferences UI should not advertise a visible no-op (disable/annotate the control until rendering exists).
Solidvs accent for primary buttons: §8 defaults to accent; if the neutral look wins, switchPrimarytosolid/on_solid— one place.- Zeron upstream drift: record the ported revision; keep the port faithful to ease future diffing.
- Web target: Phase 6 file IO must stay wasm-gated; verify
coop_webbuilds in CI from Phase 2 onward.
13. Validation checklist (final)
cargo build(desktop),cargo build -p coop_web,cargo testworkspace-wide,cargo clippy.- Fresh install: zeron light/dark defaults; OS appearance sync; appearance toggle.
- Theme picker lists builtin families grouped correctly; selection persists per appearance across restarts.
- Accent preset override recomputes accent-derived UI (buttons, focus ring, selection) live.
- Old
.settingsloads without error (theme resets, mode preserved); round-trip saves the new format. gpui-baseprojection correct: focus ring, text selection wash, scrollbar styling, overlay backdrop follow the active variant/accent.- No references to removed roles remain (
grepfrom Phase 3 acceptance). - (If Phase 6) import/report/last-known-good flows work; web build unaffected.