# Theme Engine
## Table of Contents
1. [Overview](#1-overview)
2. [ThemeManager](#2-thememanager)
3. [Default Themes](#3-default-themes)
4. [Theme Types](#4-theme-types)
5. [Color System](#5-color-system)
6. [SimpleXTheme Composable](#6-simplextheme-composable)
7. [Platform Theme](#7-platform-theme)
8. [YAML Import/Export](#8-yaml-importexport)
9. [Source Files](#9-source-files)
## Executive Summary
The SimpleX Chat theme engine implements a four-level cascade: per-chat theme overrides take precedence over per-user overrides, which take precedence over global (app-settings) overrides, which take precedence over built-in presets. Four preset themes exist (LIGHT, DARK, SIMPLEX, BLACK), each defining a Material `Colors` palette and custom `AppColors` for chat-specific elements. Themes support wallpaper customization (preset patterns or custom images) with background and tint color overrides. Theme configuration is persisted as YAML and can be imported/exported. The `SimpleXTheme` composable wraps `MaterialTheme` with additional `CompositionLocal` providers for app colors and wallpaper.
---
## 1. Overview
Theme resolution follows a priority chain:
```
per-chat override > per-user override > global override > preset default
```
At each level, individual color properties can be overridden. Unspecified properties fall through to the next level. The resolution is performed by `ThemeManager.currentColors()`, which merges all levels into a single `ActiveTheme` containing Material `Colors`, `AppColors`, and `AppWallpaper`.
Wallpapers follow the same cascade, with additional support for preset wallpapers (built-in patterns like `SCHOOL`) and custom images. Wallpaper presets can define their own color overrides that sit between the global override and the base preset.
---
## 2. ThemeManager
[`ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) (241 lines)
A singleton `object` that manages theme state, persistence, and resolution.
### Core resolution
**`currentColors()`** ([line 57](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L57)):
```kotlin
fun currentColors(
themeOverridesForType: WallpaperType?,
perChatTheme: ThemeModeOverride?,
perUserTheme: ThemeModeOverrides?,
appSettingsTheme: List
): ActiveTheme
```
This is the core resolution function. It:
1. Determines the non-system theme name (resolving `SYSTEM` to light or dark based on `systemInDarkThemeCurrently`).
2. Selects the base theme palette (LIGHT/DARK/SIMPLEX/BLACK).
3. Finds the matching `ThemeOverrides` from `appSettingsTheme` based on wallpaper type and theme name.
4. Selects the `perUserTheme` for the current light/dark mode.
5. Resolves wallpaper preset colors if applicable.
6. Merges all color layers via `toColors()`, `toAppColors()`, and `toAppWallpaper()`.
Returns `ActiveTheme(name, base, colors, appColors, wallpaper)`.
### Theme application
**`applyTheme()`** ([line 105](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L105)):
Persists the theme name, recalculates `CurrentColors`, and updates Android system bar appearance:
```kotlin
fun applyTheme(theme: String) {
if (appPrefs.currentTheme.get() != theme) {
appPrefs.currentTheme.set(theme)
}
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
}
```
**`changeDarkTheme()`** ([line 115](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L115)):
Sets the dark mode variant (DARK, SIMPLEX, or BLACK) and recalculates colors.
### Color and wallpaper modification
**`saveAndApplyThemeColor()`** ([line 120](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L120)):
Persists a single color change to the global theme overrides:
1. Gets or creates `ThemeOverrides` for the current base theme.
2. Calls `withUpdatedColor()` to update the specific `ThemeColor`.
3. Updates `currentThemeIds` mapping.
4. Recalculates `CurrentColors`.
**`applyThemeColor()`** ([line 132](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L132)):
In-memory-only color change (for per-chat/per-user theme editing before save).
**`saveAndApplyWallpaper()`** ([line 136](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L136)):
Persists wallpaper type change. Finds or creates matching `ThemeOverrides` (matching by wallpaper type + theme name), updates the wallpaper, and persists.
### Reset
**`resetAllThemeColors()` (global)** ([line 204](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L204)):
Resets all custom colors in the current global theme override to defaults. Preserves wallpaper but clears its background and tint overrides.
**`resetAllThemeColors()` (per-chat/per-user)** ([line 213](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L213)):
In-memory reset of a `ThemeModeOverride` state.
### Import/Export
**`saveAndApplyThemeOverrides()`** ([line 188](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L188)):
Imports a complete `ThemeOverrides` (from YAML). Handles wallpaper image import (base64 to file), replaces existing override for the same type, and applies.
**`currentThemeOverridesForExport()`** ([line 92](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L92)):
Exports the fully resolved current theme as a `ThemeOverrides` with all colors filled and wallpaper image embedded as base64.
### Utility
**`colorFromReadableHex()`** ([line 224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L224)):
Parses `#AARRGGBB` hex string to `Color`.
**`toReadableHex()`** ([line 227](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L227)):
Converts `Color` to `#AARRGGBB` hex string with intelligent alpha handling.
---
## 3. Default Themes
[`Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L26):
```kotlin
enum class DefaultTheme {
LIGHT, DARK, SIMPLEX, BLACK;
companion object {
const val SYSTEM_THEME_NAME: String = "SYSTEM"
}
}
```
| Theme | `mode` | Description |
|---|---|---|
| `LIGHT` | LIGHT | Standard light theme with white/light gray surfaces |
| `DARK` | DARK | Standard dark theme with dark gray surfaces |
| `SIMPLEX` | DARK | SimpleX branded dark theme with deep blue background and cyan accent |
| `BLACK` | DARK | AMOLED-optimized pure black theme |
`SYSTEM` is a virtual theme name that resolves to LIGHT or the configured dark variant at runtime.
`DefaultThemeMode` ([line 46](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L46)): `LIGHT` or `DARK`, serialized as `"light"` / `"dark"`.
---
## 4. Theme Types
### AppColors (line 53)
[`Theme.kt` L53](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L53):
```kotlin
@Stable
class AppColors(
title: Color,
primaryVariant2: Color,
sentMessage: Color,
sentQuote: Color,
receivedMessage: Color,
receivedQuote: Color,
)
```
Mutable state properties (for efficient recomposition) representing chat-specific colors not covered by Material's `Colors`.
### AppWallpaper (line 106)
[`Theme.kt` L106](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L106):
```kotlin
@Stable
class AppWallpaper(
background: Color? = null,
tint: Color? = null,
type: WallpaperType = WallpaperType.Empty,
)
```
Represents the active wallpaper state with optional background color, tint overlay, and wallpaper type (Empty, Preset, or Image).
### ThemeColor (line 140)
Enum of all customizable color slots:
`PRIMARY`, `PRIMARY_VARIANT`, `SECONDARY`, `SECONDARY_VARIANT`, `BACKGROUND`, `SURFACE`, `TITLE`, `SENT_MESSAGE`, `SENT_QUOTE`, `RECEIVED_MESSAGE`, `RECEIVED_QUOTE`, `PRIMARY_VARIANT2`, `WALLPAPER_BACKGROUND`, `WALLPAPER_TINT`
Each has a `fromColors()` method to extract the current value and a `text` property for UI display.
### ThemeColors (line 183)
[`Theme.kt` L183](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L183):
Serializable data class with optional hex color strings for each slot. Uses `@SerialName` annotations for YAML compatibility (`accent` for `primary`, `accentVariant` for `primaryVariant`, `menus` for `surface`, etc.).
### ThemeWallpaper (line 224)
[`Theme.kt` L224](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L224):
```kotlin
@Serializable
data class ThemeWallpaper(
val preset: String? = null, // Preset wallpaper name
val scale: Float? = null, // Wallpaper scale factor
val scaleType: WallpaperScaleType? = null, // Fill/fit mode
val background: String? = null, // Background color hex
val tint: String? = null, // Tint overlay color hex
val image: String? = null, // Base64-encoded image (for import/export)
val imageFile: String? = null, // Local image file name
)
```
Key methods:
- `toAppWallpaper()`: Converts to runtime `AppWallpaper`.
- `withFilledWallpaperBase64()`: Embeds the image as base64 for export.
- `importFromString()`: Saves a base64 image to disk and returns a copy with `imageFile` set.
- `from(type, background, tint)`: Factory from `WallpaperType`.
### ThemeOverrides (line 304)
[`Theme.kt` L304](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L304):
```kotlin
@Serializable
data class ThemeOverrides(
val themeId: String = UUID.randomUUID().toString(),
val base: DefaultTheme,
val colors: ThemeColors = ThemeColors(),
val wallpaper: ThemeWallpaper? = null,
)
```
A complete theme override entry. Multiple can coexist (one per wallpaper type per base theme). The `themeId` is a UUID for identity tracking. Key methods:
- `isSame(type, themeName)`: Matches by wallpaper type and base theme.
- `withUpdatedColor(name, color)`: Returns a copy with one color changed.
- `toColors()`, `toAppColors()`, `toAppWallpaper()`: Merge with base theme and per-user/per-chat overrides.
### ThemeModeOverrides (line 475)
[`Theme.kt` L475](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L475):
```kotlin
@Serializable
data class ThemeModeOverrides(
val light: ThemeModeOverride? = null,
val dark: ThemeModeOverride? = null,
)
```
Container for per-user or per-chat overrides, with separate light and dark mode variants. Stored on the `User` model as `uiThemes`.
### ThemeModeOverride (line 487)
[`Theme.kt` L487](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L487):
```kotlin
@Serializable
data class ThemeModeOverride(
val mode: DefaultThemeMode = CurrentColors.value.base.mode,
val colors: ThemeColors = ThemeColors(),
val wallpaper: ThemeWallpaper? = null,
)
```
A single mode's override with colors and wallpaper. Has `withUpdatedColor()` and `removeSameColors()` (strips colors that match base defaults).
---
## 5. Color System
Four built-in color palettes, each consisting of a Material `Colors` and an `AppColors`:
### DarkColorPalette ([line 634](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L634))
| Property | Value | Notes |
|---|---|---|
| `primary` | `SimplexBlue` | `#0088ff` |
| `surface` | `#222222` | |
| `sentMessage` | `#18262E` | Dark blue-gray |
| `receivedMessage` | `#262627` | Neutral dark |
### LightColorPalette ([line 656](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L656))
| Property | Value | Notes |
|---|---|---|
| `primary` | `SimplexBlue` | `#0088ff` |
| `surface` | `White` | |
| `sentMessage` | `#E9F7FF` | Light blue |
| `receivedMessage` | `#F5F5F6` | Near-white |
### SimplexColorPalette ([line 678](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L678))
| Property | Value | Notes |
|---|---|---|
| `primary` | `#70F0F9` | Cyan |
| `primaryVariant` | `#1298A5` | Dark cyan |
| `background` | `#111528` | Deep navy |
| `surface` | `#121C37` | Dark navy |
| `title` | `#267BE5` | Blue |
### BlackColorPalette ([line 701](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L701))
| Property | Value | Notes |
|---|---|---|
| `primary` | `#0077E0` | Darker blue |
| `background` | `#070707` | Near-black |
| `surface` | `#161617` | Very dark |
| `sentMessage` | `#18262E` | Same as Dark |
| `receivedMessage` | `#1B1B1B` | Very dark |
---
## 6. SimpleXTheme Composable
[`Theme.kt` line 773](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L773):
```kotlin
@Composable
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit)
```
The root theme composable that wraps all app content:
1. **System dark mode tracking** ([line 781](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L781)): Uses `snapshotFlow` on `isSystemInDarkTheme()` to call `reactOnDarkThemeChanges()` when the system theme changes. This triggers `ThemeManager.applyTheme(SYSTEM)` if the app is in system theme mode.
2. **User theme tracking** ([line 790](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L790)): Monitors `chatModel.currentUser.value?.uiThemes` and re-applies the theme when the active user changes.
3. **MaterialTheme wrapping** ([line 797](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L797)): Provides `theme.colors` to `MaterialTheme`, plus custom `CompositionLocal` providers:
- `LocalContentColor` -- set to `MaterialTheme.colors.onBackground`
- `LocalAppColors` -- the `AppColors` instance (remembered and updated)
- `LocalAppWallpaper` -- the `AppWallpaper` instance (remembered and updated)
- `LocalDensity` -- scaled by `desktopDensityScaleMultiplier` and `fontSizeMultiplier`
4. **`SimpleXThemeOverride`** ([line 825](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L825)): A variant that accepts an explicit `ActiveTheme` for per-chat theme previews and overlays.
### CompositionLocal access
```kotlin
val MaterialTheme.appColors: AppColors // via LocalAppColors
val MaterialTheme.wallpaper: AppWallpaper // via LocalAppWallpaper
```
### Global state
`CurrentColors` ([line 727](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L727)): A `MutableStateFlow` that holds the current resolved theme. Updated by `ThemeManager.applyTheme()` and collected by `SimpleXTheme`.
`systemInDarkThemeCurrently` ([line 724](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L724)): Tracks the current system dark mode state.
---
## 7. Platform Theme
### isSystemInDarkTheme
**Android** ([`Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt)):
```kotlin
@Composable
actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme()
```
Delegates to the standard Compose function which reads `Configuration.uiMode`.
**Desktop** ([`Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt)):
```kotlin
private val detector: OsThemeDetector = OsThemeDetector.getDetector()
.apply { registerListener(::reactOnDarkThemeChanges) }
@Composable
actual fun isSystemInDarkTheme(): Boolean = try {
detector.isDark
} catch (e: Exception) {
false // Fallback for macOS exceptions
}
```
Uses the [jSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) library (`OsThemeDetector`). The detector also registers a listener that calls `reactOnDarkThemeChanges()` proactively when the OS theme changes, ensuring the app responds even outside of composition.
### reactOnDarkThemeChanges
[`Theme.kt` line 763](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L763):
```kotlin
fun reactOnDarkThemeChanges(isDark: Boolean) {
systemInDarkThemeCurrently = isDark
if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME
&& CurrentColors.value.colors.isLight == isDark) {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
}
```
Only triggers a theme switch if the app is in SYSTEM mode and the current light/dark state disagrees with the OS.
---
## 8. YAML Import/Export
Theme overrides are persisted in `themes.yaml` (located in `preferencesDir`).
### readThemeOverrides
[`Files.kt` line 125](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L125):
```kotlin
fun readThemeOverrides(): List
```
1. Reads `themes.yaml` from `preferencesDir`.
2. Parses the YAML node tree.
3. Extracts the `themes` list.
4. Deserializes each entry as `ThemeOverrides`, skipping entries that fail to parse (with error logging).
5. Calls `skipDuplicates()` to remove entries with the same type+base combination.
### writeThemeOverrides
[`Files.kt` line 151](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt#L151):
```kotlin
fun writeThemeOverrides(overrides: List): Boolean
```
1. Serializes `ThemesFile(themes = overrides)` to YAML string.
2. Writes to a temporary file in `preferencesTmpDir`.
3. Atomically moves the temp file to `themes.yaml` using `Files.move` with `REPLACE_EXISTING`.
4. Thread-safe via `synchronized(lock)`.
### YAML format
```yaml
themes:
- themeId: "uuid-string"
base: "LIGHT"
colors:
accent: "#ff0088ff"
background: "#ffffffff"
sentMessage: "#ffe9f7ff"
wallpaper:
preset: "school"
scale: 1.0
background: "#ccffffff"
tint: "#22000000"
```
Uses the [kaml](https://github.com/charleskorn/kaml) YAML library for serialization. `ThemeColors` uses `@SerialName` annotations for cross-platform YAML key compatibility (e.g., `accent` for `primary`, `menus` for `surface`).
---
## 9. Source Files
| File | Path | Lines | Description |
|---|---|---|---|
| `ThemeManager.kt` | [`common/src/commonMain/.../ui/theme/ThemeManager.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt) | 241 | Theme resolution, persistence, color/wallpaper management |
| `Theme.kt` | [`common/src/commonMain/.../ui/theme/Theme.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt) | 848 | Type definitions, color palettes, `SimpleXTheme` composable |
| `Theme.android.kt` | [`common/src/androidMain/.../ui/theme/Theme.android.kt`](../../common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt) | 6 | Android `isSystemInDarkTheme` |
| `Theme.desktop.kt` | [`common/src/desktopMain/.../ui/theme/Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt) | 25 | Desktop `isSystemInDarkTheme` via OsThemeDetector |
| `Files.kt` | [`common/src/commonMain/.../platform/Files.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt) | 191 | `readThemeOverrides()` (L125), `writeThemeOverrides()` (L151) |