Theme System

May 11, 2026 · View on GitHub

This document describes the theming system for Ferrite, covering architecture, color definitions, theme management, and integration with egui.

Overview

The theme system provides a comprehensive set of colors, fonts, and spacing values for consistent UI styling. It supports light and dark themes with proper contrast and accessibility, plus a System theme that follows the OS preference.

Ferrite accent color: Users can pick a primary accent (RGB) in Settings → Appearance and on the Welcome screen. It replaces the default blue across most “brand” UI chrome; hyperlink colors in rendered markdown stay a fixed classic blue (theme::accent::standard_link_color) so links remain recognizable.

Ferrite accent color (user-configurable)

Settings and persistence

  • Field: Settings.accent_color: [u8; 3] (serde default matches theme::accent::DEFAULT_ACCENT_RGB).
  • Helper: Settings::ferrite_accent_rgb()egui::Color32 for UI code.
  • Sync: ThemeManager::sync_accent_rgb(accent_color) runs when settings apply so egui Visuals (selection, widgets) stay aligned.

What uses the accent

Including but not limited to:

  • Rendered markdown: H1–H6 heading color; widget chrome derived via ThemeColors::from_theme(..., accent) / EditorColors / WidgetColors.
  • Editor: Selection tint, minimap heading indicators (semantic), find-replace accents.
  • Shell UI: Active tabs, ribbon/outline/productivity highlights where wired; view mode segment (R / S / V) selected pill; Productivity Hub (Add, Start work, notes ➕, dock/detach affordances); status bar LSP summary and git branch label (via a blended status_accent for legibility).

What does not follow the accent

  • Markdown links in preview: always standard_link_color(dark_mode) — not the user accent.

Code entry points

AreaLocation
Accent math / link constantsrc/theme/accent.rs
Theme palette + apply_user_accentsrc/theme/mod.rs
Visuals buildsrc/theme/dark.rs, src/theme/light.rs, src/theme/manager.rs
View pickersrc/ui/view_segment.rs (ferrite_accent argument)
Status bar LSP / branchsrc/app/status_bar.rs
Productivity Hubsrc/ui/productivity_panel.rs (show_content, floating show)

Architecture

src/theme/
├── mod.rs       # ThemeColors struct and core types
├── accent.rs    # User accent RGB, helpers (selection_fill, panel_highlight_fill, link color)
├── colors.rs    # Color constants and utilities
├── light.rs     # Light theme egui::Visuals configuration
├── dark.rs      # Dark theme egui::Visuals configuration
└── manager.rs   # ThemeManager for runtime theme switching

Key Components

  1. ThemeColors - Main struct containing all color definitions
  2. BaseColors - Background, borders, hover states
  3. TextColors - Primary, secondary, muted, link, code text
  4. EditorThemeColors - Headings, blockquotes, code blocks, tables
  5. SyntaxColors - Code syntax highlighting colors
  6. UiColors - Accent, success, warning, error, info

ThemeManager

The ThemeManager centralizes all theme operations, handling switching, persistence, and application of themes to egui.

Basic Usage

use crate::theme::ThemeManager;
use crate::config::Theme;

// Create manager with initial theme
let mut manager = ThemeManager::new(Theme::Dark);

// Apply theme to egui context
manager.apply(&ctx);

// Switch themes
manager.set_theme(Theme::Light);
manager.apply(&ctx);

// Toggle between light/dark
manager.toggle();
manager.apply(&ctx);

// Cycle through all themes: Light -> Dark -> System
manager.cycle();
manager.apply(&ctx);

Efficient Theme Updates

// In your update loop, use apply_if_needed() for efficiency
// This only applies theme when changed or when System theme detects OS change
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    self.theme_manager.apply_if_needed(ctx);
    // ... rest of UI
}

Theme Persistence

The ThemeManager works with Settings for persistence:

// When theme changes:
fn handle_set_theme(&mut self, theme: Theme, ctx: &egui::Context) {
    self.theme_manager.set_theme(theme);
    self.theme_manager.apply(ctx);
    
    // Save to settings
    self.state.settings.theme = theme;
    self.state.mark_settings_dirty();
}

Keyboard Shortcuts

  • Ctrl+Shift+T: Cycle through themes (Light → Dark → System → Light)

Themes are accessible via View > Theme menu with options:

  • ☀ Light
  • 🌙 Dark
  • 💻 System

Usage

Getting Theme Colors

use crate::theme::ThemeColors;
use crate::config::Theme;

// Get colors for a specific theme
let colors = ThemeColors::light();
let colors = ThemeColors::dark();

// Get colors based on user setting and Ferrite accent (from Settings)
let accent = settings.ferrite_accent_rgb();
let colors = ThemeColors::from_theme(theme, &ctx.style().visuals, accent);

// Use colors in UI
ui.label(RichText::new("Hello").color(colors.text.primary));

Applying Theme to egui

use crate::theme::ThemeColors;

// Convert theme colors to egui Visuals
let colors = ThemeColors::dark();
let visuals = colors.to_visuals();
ctx.set_visuals(visuals);

// Or use the convenience method (pass user accent Color32)
let accent = settings.ferrite_accent_rgb();
let visuals =
    ThemeColors::visuals_for_theme(Theme::Dark, &ctx.style().visuals, accent);
ctx.set_visuals(visuals);

Using Color Constants

use crate::theme::{DARK_BACKGROUND, LIGHT_TEXT, ACCENT_BLUE_DARK};

// Direct color access
let bg = DARK_BACKGROUND;
let text = LIGHT_TEXT;
let accent = ACCENT_BLUE_DARK;

Using Color Utilities

use crate::theme::{blend_colors, darken, lighten, with_alpha, contrast_ratio};

// Blend two colors (0.0 = first, 1.0 = second)
let mixed = blend_colors(Color32::WHITE, Color32::BLACK, 0.5);

// Darken/lighten by percentage
let darker = darken(color, 0.2);  // 20% darker
let lighter = lighten(color, 0.2);  // 20% lighter

// Set alpha (0-255)
let semi_transparent = with_alpha(color, 128);

// Check contrast ratio for accessibility
let ratio = contrast_ratio(text_color, bg_color);
assert!(ratio > 4.5);  // WCAG AA minimum

Color Categories

Base Colors

ColorLightDarkPurpose
background#FFFFFF#1E1E1EPrimary background
background_secondary#FAFAFA#252525Panels, cards
background_tertiary#F5F5F5#2D2D2DInputs, code blocks
border#C8C8C8#3C3C3CPrimary borders
border_subtle#E6E6E6#323232Dividers
hover#F0F0F0#323232Hover state
selected#E6F0FF#283C50Selection

Text Colors

ColorLightDarkPurpose
primary#1E1E1E#DCDCDCMain content
secondary#505050#B4B4B4Descriptions
muted#787878#8C8C8CHints, placeholders
disabled#A0A0A0#646464Disabled text
link#0064B4#64B4FFHyperlinks
code#505050#C8C896Inline code

Editor Colors

ColorLightDarkPurpose
heading#0064B4#64B4FFH1-H6 headings
blockquote_border#C8C8C8#505050Quote borders
blockquote_text#646464#B4B4B4Quote text
code_block_bg#E9ECEF#23272ECode backgrounds
horizontal_rule#C8C8C8#505050HR elements
list_marker#646464#969696Bullets, numbers
table_border#C8CDD2#3C414BTable borders
table_header_bg#F0F2F5#2D323CTable headers

Syntax Colors

TokenLightDarkDescription
keyword#AF00AF#C678DDif, else, fn
string#008000#98C379String literals
number#008080#D19A66Numeric literals
comment#808080#5C6370Comments
function#0000AF#61AFEFFunction names
type_name#006496#E5C07BTypes/classes
variable#323232#E06C75Variables
operator#505050#ABB2BFOperators

UI Feedback Colors

ColorLightDarkPurpose
accent#0078D4#64B4FFPrimary actions
accent_hover#0064B4#82C8FFHover state
success#28A745#4BD264Success states
warning#FFC107#FFD232Warnings
error#DC3545#FF6464Errors
info#17A2B8#50C8DCInformation

Theme Selection

The Theme enum in config::settings controls theme selection:

pub enum Theme {
    Light,   // Always use light theme
    Dark,    // Always use dark theme
    System,  // Follow system preference
}

When System is selected, the theme follows egui's Visuals::dark_mode flag.

Accessibility

Both themes are designed for WCAG AA compliance:

  • Light theme: Minimum 4.5:1 contrast ratio for normal text
  • Dark theme: Minimum 4.5:1 contrast ratio for normal text
  • Link colors: Distinguishable from regular text
  • Error/warning colors: Sufficient contrast against backgrounds

Use contrast_ratio() to verify contrast:

let ratio = contrast_ratio(colors.text.primary, colors.base.background);
assert!(ratio >= 4.5, "Text contrast too low!");

Extending the Theme

Adding New Colors

  1. Add field to appropriate struct in mod.rs
  2. Define light/dark values in respective ::light() and ::dark() methods
  3. Update light.rs and dark.rs to apply to egui Visuals if needed

Creating Custom Themes

impl ThemeColors {
    pub fn custom() -> Self {
        Self {
            base: BaseColors {
                background: Color32::from_rgb(20, 20, 30),
                // ... custom values
            },
            // ... other categories
        }
    }
}

Files

FilePurpose
src/theme/mod.rsCore types: ThemeColors, BaseColors, TextColors, etc.
src/theme/accent.rsUser accent RGB, standard_link_color, blends (selection_fill, etc.)
src/theme/colors.rsColor constants and utility functions
src/theme/light.rsLight theme → egui::Visuals conversion
src/theme/dark.rsDark theme → egui::Visuals conversion
src/theme/manager.rsThemeManager for runtime switching and persistence

Tests

Run theme tests:

cargo test theme::

Key test coverage:

  • Light/dark theme color values
  • Theme detection (is_dark())
  • egui Visuals conversion
  • Color contrast validation
  • Color blending utilities
  • Shadow and rounding values