dampen_dev/theme_loader.rs
1//! Theme file discovery and loading for dampen-dev.
2//!
3//! This module provides functions for discovering theme.dampen files
4//! and loading them into ThemeContext for runtime use.
5//!
6//! Note: System theme detection is handled by Iced's built-in `system::theme_changes()`
7//! subscription, which uses winit's native theme detection. The initial theme is
8//! determined by the theme document's `default_theme` setting, and then updated
9//! reactively when the system theme changes.
10
11use dampen_core::ir::theme::ThemeDocument;
12use dampen_core::parser::theme_parser::parse_theme_document;
13use dampen_core::state::ThemeContext;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17/// Discover and load theme.dampen from a project directory.
18///
19/// This function looks for `src/ui/theme/theme.dampen` in the given
20/// project directory and loads it if found.
21///
22/// Note: The initial theme is set to the document's `default_theme`. System theme
23/// preference will be applied reactively via the `watch_system_theme()` subscription
24/// once the application starts.
25///
26/// # Arguments
27///
28/// * `project_dir` - The root directory of the Dampen project
29///
30/// # Returns
31///
32/// * `Ok(Some(ThemeContext))` - If a valid theme file was found and loaded
33/// * `Ok(None)` - If no theme file was found (use default Iced theme)
34/// * `Err(_)` - If a theme file exists but couldn't be parsed
35pub fn load_theme_context(project_dir: &Path) -> Result<Option<ThemeContext>, ThemeLoadError> {
36 load_theme_context_with_preference(project_dir, None)
37}
38
39/// Discover and load theme.dampen with an optional system preference.
40///
41/// This variant allows passing a pre-detected system preference for cases
42/// where it's already known (e.g., from a previous theme change event).
43///
44/// # Arguments
45///
46/// * `project_dir` - The root directory of the Dampen project
47/// * `system_preference` - Optional system theme preference ("light" or "dark")
48///
49/// # Returns
50///
51/// * `Ok(Some(ThemeContext))` - If a valid theme file was found and loaded
52/// * `Ok(None)` - If no theme file was found (use default Iced theme)
53/// * `Err(_)` - If a theme file exists but couldn't be parsed
54pub fn load_theme_context_with_preference(
55 project_dir: &Path,
56 system_preference: Option<&str>,
57) -> Result<Option<ThemeContext>, ThemeLoadError> {
58 match discover_theme_file(project_dir) {
59 Some(Ok(doc)) => {
60 let ctx = ThemeContext::from_document(doc, system_preference)
61 .map_err(ThemeLoadError::InvalidDocument)?;
62 Ok(Some(ctx))
63 }
64 Some(Err(e)) => Err(ThemeLoadError::ParseError(e)),
65 None => Ok(None),
66 }
67}
68
69/// Discover theme.dampen file in a project directory.
70///
71/// Looks for `src/ui/theme/theme.dampen` in the given project directory.
72///
73/// # Arguments
74///
75/// * `project_dir` - The root directory of the Dampen project
76///
77/// # Returns
78///
79/// * `Some(Ok(ThemeDocument))` - If a valid theme file was found
80/// * `Some(Err(_))` - If the file exists but couldn't be parsed
81/// * `None` - If no theme file was found
82pub fn discover_theme_file(project_dir: &Path) -> Option<Result<ThemeDocument, String>> {
83 let theme_path = find_theme_file_path(project_dir)?;
84
85 if !theme_path.exists() {
86 return None;
87 }
88
89 match fs::read_to_string(&theme_path) {
90 Ok(content) => match parse_theme_document(&content) {
91 Ok(doc) => Some(Ok(doc)),
92 Err(e) => Some(Err(format!("Failed to parse theme: {}", e))),
93 },
94 Err(e) => Some(Err(format!("Failed to read theme file: {}", e))),
95 }
96}
97
98/// Find the path to theme.dampen in a project directory.
99///
100/// Searches in order:
101/// 1. `src/ui/theme/theme.dampen`
102///
103/// # Arguments
104///
105/// * `project_dir` - The root directory of the Dampen project
106///
107/// # Returns
108///
109/// The path to theme.dampen if found, None otherwise
110pub fn find_theme_file_path(project_dir: &Path) -> Option<PathBuf> {
111 let paths = vec![project_dir.join("src/ui/theme/theme.dampen")];
112
113 paths.into_iter().find(|path| path.exists())
114}
115
116/// Create a minimal Dampen application structure for testing.
117///
118/// This helper function creates the basic directory structure of a
119/// Dampen application, optionally with or without a theme file.
120///
121/// # Arguments
122///
123/// * `dir` - The temporary directory to create the app structure in
124pub fn create_minimal_dampen_app(dir: &Path) {
125 let src_dir = dir.join("src");
126 let ui_dir = src_dir.join("ui");
127 let theme_dir = ui_dir.join("theme");
128
129 let _ = fs::create_dir_all(&theme_dir);
130}
131
132/// Errors that can occur when loading themes
133#[derive(Debug, thiserror::Error)]
134pub enum ThemeLoadError {
135 /// The theme file exists but couldn't be parsed
136 #[error("Failed to parse theme file: {0}")]
137 ParseError(String),
138
139 /// The parsed theme document is invalid
140 #[error("Invalid theme document: {0}")]
141 InvalidDocument(#[from] dampen_core::ir::theme::ThemeError),
142}