| name | editor-theme-migration |
| description | Pattern for migrating a monolithic style module (style.rs) to a modular theme system with incremental panel migration in an egui-based editor. |
| source | auto-skill |
| extracted_at | 2026-06-14T09:27:25.957Z |
Editor Theme Migration Pattern
Context
Titan editor had a monolithic style.rs (907 lines) with a single Palette struct and three hardcoded themes (Dark/Light/HighContrast). Needed migration to a modular theme/ system supporting 6 default themes, custom theme import/export, and incremental panel migration.
File Structure
crates/titan-editor/src/
├── style.rs # Legacy — kept alive during migration
├── theme/
│ ├── mod.rs # ThemeManager, global palette accessors, boot
│ ├── palette.rs # Palette struct + 6 defaults + PaletteBuilder
│ ├── density.rs # Spacing/radius overrides per theme
│ ├── serialization.rs # RON import/export for .ttheme files
│ └── widgets.rs # Panel frame, header, section, buttons
└── [panel files] # Migrate from style::palette:: → theme::palette_accessors::
Key Decisions
1. Global Palette Accessors as Migration Bridge
Create theme::palette_accessors module with functions that read from a global RwLock<Palette>. This lets panel files switch from style::palette::* to theme::palette_accessors::* without changing call patterns:
static ACTIVE_THEME_PALETTE: std::sync::RwLock<Palette> =
std::sync::RwLock::new(Palette::default_catppuccin_mocha());
pub mod palette_accessors {
use super::active_palette;
pub fn text_secondary() -> Color32 { active_palette().text_secondary }
}
Set the global palette in Theme::apply_to_egui:
pub fn apply_to_egui(&self, ctx: &egui::Context) {
set_active_palette(self.palette);
ctx.set_visuals(self.build_visuals());
apply_text_and_spacing(ctx);
}
2. Batch Migration with PowerShell
On Windows, use PowerShell to bulk-replace across all panel files:
cd C:\Dev\Titan\crates\titan-editor\src
# Replace palette accessors
powershell -Command "$files = Get-ChildItem -Recurse -Filter '*.rs'; foreach ($f in $files) { $content = Get-Content $f.FullName -Raw; if ($content -match 'style::palette::') { ($content -replace 'style::palette::', 'theme::palette_accessors::') | Set-Content $f.FullName -NoNewline } }"
# Replace widget calls
powershell -Command "$files = Get-ChildItem -Recurse -Filter '*.rs'; foreach ($f in $files) { $content = Get-Content $f.FullName -Raw; if ($content -match 'style::widgets::header') { ($content -replace 'style::widgets::header', 'theme::widgets::header') | Set-Content $f.FullName -NoNewline } }"
3. Panel File Import Updates
After bulk replacement, add theme to the use crate::{} import in each panel file:
powershell -Command "$files = @('asset_browser.rs','asset_editor_container.rs','...'); foreach ($f in $files) { $content = Get-Content $f.FullName -Raw; if ($content -match 'theme::palette_accessors') { $content = $content -replace '(use crate::\{[^}]*)\};', '$1, theme};'; $content | Set-Content $f.FullName -NoNewline } }"
4. Fix Double crate::crate:: from Over-zealous Replacement
The PowerShell replacement sometimes creates crate::crate::theme::... inside modules that already use crate::. Fix with:
powershell -Command "(Get-Content lib.rs -Raw) -replace 'crate::crate::', 'crate::' | Set-Content lib.rs -NoNewline"
5. egui Closure Borrow Issues
When using theme_manager inside an egui closure (show(..., |ui| { ... })), the closure is FnMut and captures theme_manager by move. Use ref mut to borrow inside the closure:
if let Some(tm) = theme_manager { tm.add_custom_theme(theme); }
if let Some(ref mut tm) = theme_manager { tm.add_custom_theme(theme); }
6. Dead Code Cleanup After Migration
After all panels migrate to theme::palette_accessors, remove:
- The old
preferences::Theme enum (Dark/Light/HighContrast)
- The backward-compat
deserialize_theme_name deserializer
- The
legacy_theme_to_name migration helper
- Keep
style.rs for now — it still provides apply_titan_style for boot-time initialization and tests
7. Theme Import/Export UI in Preferences
Use rfd::FileDialog for file picker dialogs. The import flow:
- Pick
.ttheme file → std::fs::read_to_string → crate::theme::serialization::deserialize_theme
theme_manager.add_custom_theme(theme)
- Save to disk via
serialization::save_theme_to_disk
- Set active and apply to egui context
8. Layout Management
Add DeleteLayout { name: String } variant to LayoutAction enum. The reducer calls layout::delete_layout(root, &name) which removes the file. Add a "Delete" button to the layout flyout menu (... button in Window > Layout).
What NOT to Do
- Don't try to bridge
style::Palette and theme::palette::Palette — they're distinct types and the compiler will catch it. Use style::Palette::dark() for boot-time only, let Theme::apply_to_egui handle runtime.
- Don't skip the global palette accessor bridge — it saves hours of per-panel edits.
- Don't delete
style.rs until all panels compile and tests pass with the new system.