use crate::css::{CssParser, StyleSheet, StyleRule};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Default)]
pub struct StyleManager {
default_styles: StyleSheet,
theme_styles: StyleSheet,
custom_styles: Vec<StyleSheet>,
modules: HashMap<String, StyleSheet>,
variables: HashMap<String, String>,
watch_files: bool,
loaded_files: Vec<String>,
}
impl StyleManager {
pub fn new() -> Self {
let mut manager = Self::default();
manager.load_default_styles();
manager
}
pub fn empty() -> Self {
Self::default()
}
fn load_default_styles(&mut self) {
let default_css = include_str!("default.css");
match CssParser::parse_stylesheet(default_css) {
Ok(sheet) => {
self.default_styles = sheet;
}
Err(e) => {
log::warn!("Failed to parse default CSS: {:?}", e);
}
}
}
pub fn load_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), CssLoadError> {
let path = path.as_ref();
let css = fs::read_to_string(path)
.map_err(|e| CssLoadError::FileRead {
path: path.display().to_string(),
error: e.to_string(),
})?;
let sheet = CssParser::parse_stylesheet(&css)
.map_err(|e| CssLoadError::Parse {
source: path.display().to_string(),
error: format!("{:?}", e),
})?;
self.custom_styles.push(sheet);
self.loaded_files.push(path.display().to_string());
Ok(())
}
pub fn load_css(&mut self, css: &str) -> Result<(), CssLoadError> {
let sheet = CssParser::parse_stylesheet(css)
.map_err(|e| CssLoadError::Parse {
source: "<inline>".to_string(),
error: format!("{:?}", e),
})?;
self.custom_styles.push(sheet);
Ok(())
}
pub fn load_module(&mut self, name: &str, css: &str) -> Result<(), CssLoadError> {
let sheet = CssParser::parse_stylesheet(css)
.map_err(|e| CssLoadError::Parse {
source: format!("module:{}", name),
error: format!("{:?}", e),
})?;
self.modules.insert(name.to_string(), sheet);
Ok(())
}
pub fn load_module_file<P: AsRef<Path>>(&mut self, name: &str, path: P) -> Result<(), CssLoadError> {
let path = path.as_ref();
let css = fs::read_to_string(path)
.map_err(|e| CssLoadError::FileRead {
path: path.display().to_string(),
error: e.to_string(),
})?;
self.load_module(name, &css)
}
pub fn unload_module(&mut self, name: &str) -> bool {
self.modules.remove(name).is_some()
}
pub fn has_module(&self, name: &str) -> bool {
self.modules.contains_key(name)
}
pub fn set_theme_styles(&mut self, css: &str) -> Result<(), CssLoadError> {
let sheet = CssParser::parse_stylesheet(css)
.map_err(|e| CssLoadError::Parse {
source: "<theme>".to_string(),
error: format!("{:?}", e),
})?;
self.theme_styles = sheet;
Ok(())
}
pub fn set_variable(&mut self, name: &str, value: &str) {
self.variables.insert(name.to_string(), value.to_string());
}
pub fn get_variable(&self, name: &str) -> Option<&String> {
self.variables.get(name)
}
pub fn set_variables(&mut self, vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) {
for (name, value) in vars {
self.variables.insert(name.into(), value.into());
}
}
pub fn combined_stylesheet(&self) -> StyleSheet {
let mut combined = StyleSheet::default();
combined.merge(self.default_styles.clone());
combined.merge(self.theme_styles.clone());
for sheet in self.modules.values() {
combined.merge(sheet.clone());
}
for sheet in &self.custom_styles {
combined.merge(sheet.clone());
}
combined
}
pub fn get_rules(&self, pattern: &str) -> Vec<StyleRule> {
let combined = self.combined_stylesheet();
combined.rules
.into_iter()
.filter(|rule| {
rule.selector.parts.iter().any(|part| {
match part {
crate::css::SelectorPart::Class(name) => name.contains(pattern),
crate::css::SelectorPart::Type(name) => name.contains(pattern),
crate::css::SelectorPart::Id(name) => name.contains(pattern),
_ => false,
}
})
})
.collect()
}
pub fn clear_custom(&mut self) {
self.custom_styles.clear();
self.loaded_files.clear();
}
pub fn clear_all(&mut self) {
self.default_styles = StyleSheet::default();
self.theme_styles = StyleSheet::default();
self.custom_styles.clear();
self.modules.clear();
self.variables.clear();
self.loaded_files.clear();
}
pub fn reload_files(&mut self) -> Result<(), CssLoadError> {
let files = self.loaded_files.clone();
self.custom_styles.clear();
self.loaded_files.clear();
for file in files {
self.load_file(&file)?;
}
Ok(())
}
pub fn enable_watch(&mut self) {
self.watch_files = true;
}
pub fn disable_watch(&mut self) {
self.watch_files = false;
}
pub fn stylesheet_count(&self) -> usize {
1 + (if self.theme_styles.rules.is_empty() { 0 } else { 1 }) +
self.modules.len() +
self.custom_styles.len()
}
pub fn rule_count(&self) -> usize {
self.default_styles.rules.len() +
self.theme_styles.rules.len() +
self.modules.values().map(|s| s.rules.len()).sum::<usize>() +
self.custom_styles.iter().map(|s| s.rules.len()).sum::<usize>()
}
}
#[derive(Debug, Clone)]
pub enum CssLoadError {
FileRead {
path: String,
error: String,
},
Parse {
source: String,
error: String,
},
InvalidValue {
property: String,
value: String,
},
}
impl std::fmt::Display for CssLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CssLoadError::FileRead { path, error } => {
write!(f, "Failed to read CSS file '{}': {}", path, error)
}
CssLoadError::Parse { source, error } => {
write!(f, "Failed to parse CSS from {}: {}", source, error)
}
CssLoadError::InvalidValue { property, value } => {
write!(f, "Invalid value '{}' for property '{}'", value, property)
}
}
}
}
impl std::error::Error for CssLoadError {}
pub struct StyleBuilder {
manager: StyleManager,
}
impl StyleBuilder {
pub fn new() -> Self {
Self {
manager: StyleManager::new(),
}
}
pub fn empty() -> Self {
Self {
manager: StyleManager::empty(),
}
}
pub fn file<P: AsRef<Path>>(mut self, path: P) -> Result<Self, CssLoadError> {
self.manager.load_file(path)?;
Ok(self)
}
pub fn css(mut self, css: &str) -> Result<Self, CssLoadError> {
self.manager.load_css(css)?;
Ok(self)
}
pub fn module(mut self, name: &str, css: &str) -> Result<Self, CssLoadError> {
self.manager.load_module(name, css)?;
Ok(self)
}
pub fn var(mut self, name: &str, value: &str) -> Self {
self.manager.set_variable(name, value);
self
}
pub fn vars(mut self, vars: impl IntoIterator<Item = (&'static str, &'static str)>) -> Self {
for (name, value) in vars {
self.manager.set_variable(name, value);
}
self
}
pub fn build(self) -> StyleManager {
self.manager
}
}
impl Default for StyleBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_css_string() {
let mut manager = StyleManager::new();
let result = manager.load_css(r#"
.test-class {
color: red;
padding: 10px;
}
"#);
assert!(result.is_ok());
assert!(manager.rule_count() > 0);
}
#[test]
fn test_css_variables() {
let mut manager = StyleManager::new();
manager.set_variable("--primary", "#3b82f6");
manager.set_variable("--radius", "8px");
assert_eq!(manager.get_variable("--primary"), Some(&"#3b82f6".to_string()));
assert_eq!(manager.get_variable("--radius"), Some(&"8px".to_string()));
assert_eq!(manager.get_variable("--unknown"), None);
}
#[test]
fn test_modules() {
let mut manager = StyleManager::new();
manager.load_module("buttons", ".btn { padding: 8px; }").unwrap();
assert!(manager.has_module("buttons"));
manager.unload_module("buttons");
assert!(!manager.has_module("buttons"));
}
#[test]
fn test_style_builder() {
let manager = StyleBuilder::new()
.css(".custom { color: blue; }").unwrap()
.var("--accent", "#f00")
.build();
assert!(manager.rule_count() > 0);
assert_eq!(manager.get_variable("--accent"), Some(&"#f00".to_string()));
}
#[test]
fn test_clear_custom() {
let mut manager = StyleManager::new();
let initial_count = manager.rule_count();
manager.load_css(".extra { color: green; }").unwrap();
assert!(manager.rule_count() > initial_count);
manager.clear_custom();
assert_eq!(manager.rule_count(), initial_count);
}
}