Files
simplex-chat/apps/ios/spec/services/theme.md
T
Evgeny @ SimpleX ChatandEvgeny Poberezkin c3c733e0d4 ios, ui, ci: terminal chat layout in the iOS app (#7898)
* plan: UI improvements to improve LLM usage via SimpleX

* core, ui: types for chat layout

* core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots

* plan: message priority

* fixes

* query plan

* update plans

* remove silent messages plan

* rename migrations

* implement layout

* amend

* fix

* fix

* fix

* improve

* time on the right, sender name in blue

* no avatar in direct chats

* better previews

* update, fixes

* update

* time gap

* changes

* ui: terminal layout line heights, gaps and time placement

* ui: simplify terminal meta layout, correct its docs and tests

* ui: right-to-left emoji edge in terminal rows, time placement tests

* android, desktop: raise Kotlin compiler memory to 4 GB

* ui: terminal captions as follow-up text, classic event separators

* ui: terminal media edge and overlay time, plan decisions

* ui: simplify terminal row separation, correct doc line references

* ui: keep the media time inside terminal rows, restore caption conditions

* ui: terminal heading capitals at large font scales, idle live message time

* ui: rename the classic voice-with-text flag, correct the meta copy docs

* ui: smaller terminal layout diff, sender gap and meta names

* ui: terminal row as tall as the media time

* docs: remove the chat layout section from the bots readme

* docs: shorten the terminal layout section of the chat view

* core: address settings API as on master, /auto_accept reads the stored layout

* ui: smaller terminal headings

* ui: classic sender rows and avatar rule as on master

* ui: classic reply quote text colour as on master

* ios: keep the chat layout setting with other UI settings

* ui: go-to button at the end of terminal rows, no gutter in direct chats

* ui: forwarded arrow on the header line of terminal rows, above the time

* ui: forwarded arrow centred above the time line of terminal rows

* ios: terminal chat layout, chosen per app, profile and chat

* ios: event time reserve of three spaces, app layout picker as Kotlin

* ios: spec and plan of the terminal chat layout

* ios: classic message text at 87% in dark themes, as terminal rows

* ios: terminal gaps scaled by the body size, 17/16 of Kotlin values

* ios: terminal quote sender at 13.5pt, thumbnail at the item end

* ios: product docs of the terminal layout, plans updated for iOS

* ios: chat scenes rendered to PNG by a unit test in the simulator CI

* ios: snapshots rendered by the smoke test script, workflow unchanged

* ios: terminal names at 13.5 x 17/16 pt, as the gaps

* ios: Markdown views at the 87% message colour in dark themes

* ios: terminal quote thumbnail 12pt after the quote text or the bar

* ios: terminal rule-only message widened by a wider quote or header

* ios: member avatars scaled with the text size, as in the chat list

* ios: snapshot rows at their own height, as list cells, so text wraps

* ios: member avatars back at 34pt, for the snapshot before scaling

* ios: member avatars scaled with the text size, as in the chat list

* ui: terminal quote thumbnail 12dp after the quote text or the bar

* ios: classic group bubble width follows the scaled member avatar

* ci: ios-sim README describes the snapshots rendered after the smoke test

* ios: scratch, padding variants for the I7 and I8 renders, to revert

* ios: revert the scratch padding variants after their renders

* ui: contrast of the voice message button in terminal rows

* ios: scratch, spacing variants and list margins to render, to revert

* ios: revert the scratch spacing variants after their renders

* ios: snapshots at a fixed date, in UTC and the en_US locale

* ios: snapshots drawn as 8-bit sRGB without alpha

* ci: compare iOS snapshots with the references in the repository

* ci: record and check iOS snapshot references on GitHub's runner

* ios: scratch, list margins on the remaining screens, to revert

* ios: revert the scratch list margins after their measurement

* ci: snapshot scripts executable, as the other CI scripts

* ios: scratch, voice circle variants in light themes, to revert

* ios: revert the scratch voice circle variants

* ios: one 12pt padding at the edges and after avatars in terminal rows

* ios: classic group sender name on the bubble edge

* ui: one 12dp padding at the edges and after avatars in terminal rows

* ios: one heading scale in both chat layouts

* ios: snapshot references of the chat scenes

* docs: leave the headings and classic lines of the plan to ep/classic-bubbles

* ios: declare terminal layout functions inside types

* ios: keep layout constants at file scope

* ios: keep file-scope functions, move only extension members into types

* ios: snapshot tests record row heights at first layout and the appearance layout switch

* ios: appearance preview follows the chat layout setting at once

* ios: snapshot test measures first-layout rows by SwiftUI and by cells

* ios: chat rows measured at the chat width when they first appear

* ios: snapshot test checks that chat rows keep their first height after a reload

* ios, ci: references of terminal and classic chat scenes, dark message text

* ios: message text at 95% in dark themes

* ios, ci: references with message text at 95% in dark themes

* ios: media in terminal rows end at the time's right inset

* ios, ci: references with terminal media at the time's right inset

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2026-10-29 12:45:34 +00:00

19 KiB

SimpleX Chat iOS -- Theme Engine

Technical specification for the theming system: ThemeManager, default themes, customization layers, wallpapers, and YAML export.

Related specs: State Management | Architecture | README Related product: Product Overview

Source: ThemeManager.swift | AppearanceSettings.swift | ThemeTypes.swift | ChatWallpaperTypes.swift | Theme.swift


Table of Contents

  1. Overview
  2. ThemeManager
  3. Default Themes
  4. Customization Layers
  5. Color System
  6. Wallpapers
  7. Chat Bubble Styling
  8. Color Scheme Mode
  9. YAML Export/Import

1. Overview

The theme engine provides a layered customization system where themes can be overridden at multiple levels: global defaults, per-user, and per-chat.

Theme Resolution Order (most specific wins):
┌─────────────────────┐
│  Per-chat override   │  apiSetChatUIThemes(chatId:, themes:)
├─────────────────────┤
│  Per-user override   │  apiSetUserUIThemes(userId:, themes:)
├─────────────────────┤
│  App settings theme  │  themeOverridesDefault (UserDefaults)
├─────────────────────┤
│  Base theme          │  Light / Dark / SimpleX / Black
└─────────────────────┘

The resolved theme is published as AppTheme.shared and consumed by all SwiftUI views via @EnvironmentObject.


2. ThemeManager (L15)

File: Shared/Theme/ThemeManager.swift

Static utility class that resolves the current theme by merging all customization layers.

ActiveTheme

The resolved theme output:

struct ActiveTheme: Equatable {
    let name: String            // Theme name (e.g., "light", "dark", "simplex", "black", "system")
    let base: DefaultTheme      // Base theme enum
    let colors: Colors          // Resolved color palette
    let appColors: AppColors    // App-specific colors (sent/received bubbles, etc.)
    var wallpaper: AppWallpaper // Resolved wallpaper
}

Key Static Methods

Method Purpose Line
applyTheme(_:) Apply a theme by name, updates AppTheme.shared L124
currentColors(...) Resolve full theme from all layers L64
defaultActiveTheme(_:) Get default theme override from app settings L48
currentThemeOverridesForExport(...) Get current overrides for YAML export L105
adjustWindowStyle() Adjust window style after theme change L136
changeDarkTheme(_:) Change the dark theme variant L166
saveAndApplyThemeColor(...) Save and apply a theme color override L173
applyThemeColor(...) Apply a theme color to a binding L186
saveAndApplyWallpaper(...) Save and apply a wallpaper change L191
copyFromSameThemeOverrides(...) Copy overrides from matching theme L213
applyWallpaper(...) Apply wallpaper to a binding L256
saveAndApplyThemeOverrides(...) Save and apply full theme overrides L267
resetAllThemeColors(_:) Reset all color overrides (CodableDefault) L288
resetAllThemeColors(_:) Reset all color overrides (Binding) L302
removeTheme(_:) Remove a saved theme by ID L311

Theme Resolution Algorithm

currentColors() in ThemeManager.swift:

  1. Determine base theme from currentThemeDefault:
    • If "system": use light or dark based on systemInDarkThemeCurrently
    • Dark mode maps to systemDarkThemeDefault (Dark, SimpleX, or Black)
  2. Get base color palette (LightColorPalette, DarkColorPalette, SimplexColorPalette, BlackColorPalette)
  3. Look up app settings theme override (themeOverridesDefault)
  4. Look up per-user theme override (User.uiThemes)
  5. Look up per-chat theme override (from ChatInfo)
  6. Look up wallpaper preset colors (if wallpaper has preset color overrides)
  7. Merge layers: base <- app override <- preset wallpaper colors <- per-user <- per-chat
  8. Return ActiveTheme with resolved colors, app colors, and wallpaper

3. Default Themes

Four built-in themes with pre-defined color palettes:

Theme Enum Key Characteristics
Light DefaultTheme.LIGHT White background, standard colors
Dark DefaultTheme.DARK Dark gray background, light text
SimpleX DefaultTheme.SIMPLEX Brand purple accents, dark background
Black DefaultTheme.BLACK Pure black background (OLED), high contrast

DefaultTheme Enum

enum DefaultTheme {
    case LIGHT
    case DARK
    case SIMPLEX
    case BLACK

    static let SYSTEM_THEME_NAME = "SYSTEM"

    var themeName: String { ... }
    var mode: DefaultThemeMode { ... }  // .light or .dark
}

Color Palettes

Each base theme defines two palette types:

  • Colors: Standard UI colors (primary, background, surface, error, onBackground, onSurface)
  • AppColors: App-specific colors (sentMessage, receivedMessage, title, primaryVariant2)

Palette instances:


4. Customization Layers

Layer 1: App Settings Theme

Stored in themeOverridesDefault (UserDefaults). Contains [ThemeOverrides] -- an array of theme overrides, one per base theme.

ThemeOverrides

struct ThemeOverrides: Codable {
    var base: DefaultTheme
    var colors: ThemeColors?       // Color overrides
    var wallpaper: ThemeWallpaper? // Wallpaper setting
}

Layer 2: Per-User Theme

Stored on the User object (User.uiThemes: ThemeModeOverrides?), persisted in the Haskell database via apiSetUserUIThemes(userId:, themes:).

ThemeModeOverrides

struct ThemeModeOverrides: Codable {
    var light: ThemeModeOverride?
    var dark: ThemeModeOverride?
    var chatLayout: ChatLayout?
}

chatLayout: the chat layout chosen for the profile or the chat (Chat Layout below). withChatLayout(_:) sets it and returns nil for overrides left empty. Theme writes keep it: "Reset to app theme" and "Remove image" keep only the layout (layoutOnly()).

ThemeModeOverride

struct ThemeModeOverride: Codable {
    var mode: DefaultThemeMode?
    var colors: ThemeColors?
    var wallpaper: ThemeWallpaper?
    var type: WallpaperType?         // Computed from wallpaper
}

Layer 3: Per-Chat Theme

Stored per-chat via apiSetChatUIThemes(chatId:, themes:). Same ThemeModeOverrides structure.

Chat Layout

Values of ChatLayout: .classic, .terminal, .unknown(String). Terminal rows: chat view spec, section 9.

app             chatLayoutDefault (DEFAULT_CHAT_LAYOUT in UserDefaults); AppSettings.uiChatLayout
profile         User.uiThemes.chatLayout
chat            uiThemes.chatLayout of the contact or group
chat profile    Contact.chatLayout, GroupProfile.chatLayout (ChatInfo.chatLayout)

Pickers, "Chat layout":

Appearance, app destination        ChatLayoutPicker: Classic, Terminal → chatLayoutDefault
Appearance, profile destination    ChatLayoutPicker: "<app layout> (default)" → nil; other layout
                                   → themeUserDestination.withChatLayout, saveThemeToDatabase
chat theme screen                  ChatLayoutSection, first section of ChatWallpaperEditor (layoutSection)
                                   "<default> (chat)" when ChatInfo.chatLayout is the default, else "<default> (default)" → nil
                                   other layout → chat uiThemes.withChatLayout, apiSetChatUIThemes, ChatModel.updateChatInfo
app destination selected           ThemeDestinationPicker: currentUser.uiThemes = uiThemes.layoutOnly()

Override Merging

Colors are merged field-by-field: if a more-specific layer defines a color, it overrides; if nil, falls through to the next layer.


5. Color System

File: SimpleXChat/Theme/ThemeTypes.swift

ThemeColors

Overridable color definitions:

struct ThemeColors: Codable {
    var primary: String?           // Primary brand color
    var primaryVariant: String?    // Primary variant
    var secondary: String?         // Secondary color
    var secondaryVariant: String?  // Secondary variant
    var background: String?        // Main background
    var surface: String?           // Card/surface background
    var title: String?             // Title text color
    var primaryVariant2: String?   // Additional variant
    var sentMessage: String?       // Sent message bubble
    var sentQuote: String?         // Sent quote background
    var receivedMessage: String?   // Received message bubble
    var receivedQuote: String?     // Received quote background
}

Colors are stored as hex strings (e.g., "#FF6600") and converted to SwiftUI Color values at resolution time.

Colors (Resolved Palette)

struct Colors {
    var isLight: Bool
    var primary: Color
    var primaryVariant: Color
    var secondary: Color
    var secondaryVariant: Color
    var background: Color
    var surface: Color
    var error: Color
    var onBackground: Color
    var onSurface: Color
    // ... etc
}

AppColors (Resolved App-Specific)

struct AppColors {
    var title: Color
    var primaryVariant2: Color
    var sentMessage: Color
    var sentQuote: Color
    var receivedMessage: Color
    var receivedQuote: Color
}

6. Wallpapers

File: SimpleXChat/Theme/ChatWallpaperTypes.swift

Preset Wallpapers

6 built-in wallpaper presets:

Preset ID Description
Cats cats Cat-themed pattern
Flowers flowers Floral pattern
Hearts hearts Heart pattern
Kids kids Children's pattern
School school School/notebook pattern (default)
Travel travel Travel-themed pattern

Each preset defines per-theme color tints (PresetWallpaper.colors[DefaultTheme]) that subtly adjust the color palette to complement the wallpaper.

Custom Wallpapers

Users can set a custom image as wallpaper:

  • Stored in Documents/wallpapers/ directory
  • Scaled and tiled to fill the chat background
  • Custom wallpapers can be combined with color overrides

Chats in the terminal layout draw the wallpaper background colour without the image; their theme screens show presets as colours and hide the custom image tile, image scale and wallpaper tint (chat view spec, section 9).

WallpaperType

enum WallpaperType {
    case preset(filename: String, scale: Float?)   // Built-in wallpaper
    case image(filename: String, scale: Float?)    // Custom image
    case empty                                      // No wallpaper
}

AppWallpaper (Resolved)

struct AppWallpaper {
    var background: Color?      // Background color override
    var tint: Color?            // Tint/overlay color
    var type: WallpaperType
}

7. Chat Bubble Styling

Configurable bubble appearance properties:

Property Description Stored In
chatItemRoundness Corner radius of message bubbles App settings
chatItemTail Whether bubbles have a tail/arrow App settings
Avatar corner radius Roundness of profile avatars App settings

These are configured in Shared/Views/UserSettings/AppearanceSettings.swift (L26).


8. Color Scheme Mode

System Follow

When theme is set to "system" (DefaultTheme.SYSTEM_THEME_NAME):

  • Light mode: uses DefaultTheme.LIGHT palette
  • Dark mode: uses the configured dark theme (systemDarkThemeDefault), which can be Dark, SimpleX, or Black

Forced Mode

Users can force light or dark mode regardless of system setting by selecting a specific theme other than "system".

Detection

systemInDarkThemeCurrently:

var systemInDarkThemeCurrently: Bool {
    return UITraitCollection.current.userInterfaceStyle == .dark
}

ChatModel.currentUser setter triggers ThemeManager.applyTheme() to handle per-user theme overrides when switching users.


9. YAML Export/Import

Theme configurations can be exported as YAML for sharing:

Export

ThemeManager.currentThemeOverridesForExport() generates a ThemeOverrides representing the current resolved theme, which is then serialized to YAML using the Yams library.

Import

YAML theme strings are parsed back into ThemeOverrides and applied as app settings theme overrides.

Key functions in AppearanceSettings.swift:

Function Purpose Line
ImportExportThemeSection UI section for import/export controls L603
ThemeImporter ViewModifier for YAML file import L640
decodeYAML(_:) Parse YAML string into Decodable type L1150
encodeThemeOverrides(_:) Encode ThemeOverrides to YAML string L1160

Toolbar Material

ToolbarMaterial controls the navigation bar appearance:

  • Configurable opacity/material (translucent, opaque)
  • Stored in app settings

Source Files

File Path Key Definitions
Theme manager Shared/Theme/ThemeManager.swift ThemeManager (L15), ActiveTheme (L17)
Theme types & colors SimpleXChat/Theme/ThemeTypes.swift DefaultTheme (L13), Colors (L44), AppColors (L90), AppWallpaper (L142), ThemeColors (L230), ThemeWallpaper (L302), ThemeOverrides (L385), ThemeModeOverrides (L570), ThemeModeOverride (L591)
Wallpaper types SimpleXChat/Theme/ChatWallpaperTypes.swift PresetWallpaper (L13), WallpaperType (L311)
Color utilities SimpleXChat/Theme/Color.swift Hex color conversion
App theme observable Shared/Theme/Theme.swift AppTheme (L22), CurrentColors (L14), systemInDarkThemeCurrently (L95)
Appearance settings UI Shared/Views/UserSettings/AppearanceSettings.swift AppearanceSettings (L26), ToolbarMaterial (L319), ImportExportThemeSection (L603)
Theme mode editor Shared/Views/Helpers/ThemeModeEditor.swift Theme mode selection UI
Haskell theme types ../../src/Simplex/Chat/Types/UITheme.hs Server-side theme persistence