use std::collections::HashMap;
use crate::color::OpalineColor;
#[cfg(feature = "gradients")]
use crate::gradient::Gradient;
use crate::resolver::ResolvedTheme;
use crate::schema::{ThemeMeta, ThemeVariant};
use crate::style::OpalineStyle;
#[derive(Debug, Clone)]
pub struct Theme {
pub meta: ThemeMeta,
palette: HashMap<String, OpalineColor>,
tokens: HashMap<String, OpalineColor>,
styles: HashMap<String, OpalineStyle>,
#[cfg(feature = "gradients")]
gradients: HashMap<String, Gradient>,
}
impl Theme {
pub fn from_resolved(meta: ThemeMeta, resolved: ResolvedTheme) -> Self {
Self {
meta,
palette: resolved.palette,
tokens: resolved.tokens,
styles: resolved.styles,
#[cfg(feature = "gradients")]
gradients: resolved.gradients,
}
}
pub fn builder(name: impl Into<String>) -> ThemeBuilder {
ThemeBuilder::new(name)
}
pub fn color(&self, token: &str) -> OpalineColor {
self.tokens
.get(token)
.or_else(|| self.palette.get(token))
.copied()
.unwrap_or(OpalineColor::FALLBACK)
}
pub fn try_color(&self, token: &str) -> Option<OpalineColor> {
self.tokens
.get(token)
.or_else(|| self.palette.get(token))
.copied()
}
pub fn has_token(&self, name: &str) -> bool {
self.tokens.contains_key(name) || self.palette.contains_key(name)
}
pub fn token_names(&self) -> Vec<&str> {
self.tokens.keys().map(String::as_str).collect()
}
pub fn palette_names(&self) -> Vec<&str> {
self.palette.keys().map(String::as_str).collect()
}
pub fn style(&self, name: &str) -> OpalineStyle {
self.styles.get(name).cloned().unwrap_or_default()
}
pub fn try_style(&self, name: &str) -> Option<&OpalineStyle> {
self.styles.get(name)
}
pub fn has_style(&self, name: &str) -> bool {
self.styles.contains_key(name)
}
pub fn style_names(&self) -> Vec<&str> {
self.styles.keys().map(String::as_str).collect()
}
#[cfg(feature = "gradients")]
pub fn gradient(&self, name: &str, t: f32) -> OpalineColor {
self.gradients
.get(name)
.map_or(OpalineColor::FALLBACK, |g| g.at(t))
}
#[cfg(feature = "gradients")]
pub fn try_gradient(&self, name: &str, t: f32) -> Option<OpalineColor> {
self.gradients.get(name).map(|g| g.at(t))
}
#[cfg(feature = "gradients")]
pub fn get_gradient(&self, name: &str) -> Option<&Gradient> {
self.gradients.get(name)
}
#[cfg(feature = "gradients")]
pub fn has_gradient(&self, name: &str) -> bool {
self.gradients.contains_key(name)
}
#[cfg(feature = "gradients")]
pub fn gradient_names(&self) -> Vec<&str> {
self.gradients.keys().map(String::as_str).collect()
}
pub fn is_dark(&self) -> bool {
self.meta.variant == ThemeVariant::Dark
}
pub fn is_light(&self) -> bool {
self.meta.variant == ThemeVariant::Light
}
pub fn register_default_token(&mut self, name: impl Into<String>, color: OpalineColor) {
let key = name.into();
if !self.has_token(&key) {
self.tokens.insert(key, color);
}
}
pub fn register_token(&mut self, name: impl Into<String>, color: OpalineColor) {
self.tokens.insert(name.into(), color);
}
pub fn register_default_style(&mut self, name: impl Into<String>, style: OpalineStyle) {
let key = name.into();
self.styles.entry(key).or_insert(style);
}
pub fn register_style(&mut self, name: impl Into<String>, style: OpalineStyle) {
self.styles.insert(name.into(), style);
}
}
pub struct ThemeBuilder {
meta: ThemeMeta,
palette: HashMap<String, OpalineColor>,
tokens: HashMap<String, OpalineColor>,
styles: HashMap<String, OpalineStyle>,
#[cfg(feature = "gradients")]
gradients: HashMap<String, Gradient>,
}
impl ThemeBuilder {
fn new(name: impl Into<String>) -> Self {
Self {
meta: ThemeMeta::new(name),
palette: HashMap::new(),
tokens: HashMap::new(),
styles: HashMap::new(),
#[cfg(feature = "gradients")]
gradients: HashMap::new(),
}
}
#[must_use]
pub fn author(mut self, author: impl Into<String>) -> Self {
self.meta.author = Some(author.into());
self
}
#[must_use]
pub fn variant(mut self, variant: ThemeVariant) -> Self {
self.meta.variant = variant;
self
}
#[must_use]
pub fn version(mut self, version: impl Into<String>) -> Self {
self.meta.version = Some(version.into());
self
}
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.meta.description = Some(desc.into());
self
}
#[must_use]
pub fn palette(mut self, name: impl Into<String>, color: OpalineColor) -> Self {
self.palette.insert(name.into(), color);
self
}
#[must_use]
pub fn token(mut self, name: impl Into<String>, color: OpalineColor) -> Self {
self.tokens.insert(name.into(), color);
self
}
#[must_use]
pub fn style(mut self, name: impl Into<String>, style: OpalineStyle) -> Self {
self.styles.insert(name.into(), style);
self
}
#[cfg(feature = "gradients")]
#[must_use]
pub fn gradient(mut self, name: impl Into<String>, gradient: Gradient) -> Self {
self.gradients.insert(name.into(), gradient);
self
}
#[must_use]
pub fn build(self) -> Theme {
Theme {
meta: self.meta,
palette: self.palette,
tokens: self.tokens,
styles: self.styles,
#[cfg(feature = "gradients")]
gradients: self.gradients,
}
}
}
impl Default for Theme {
fn default() -> Self {
#[cfg(feature = "builtin-themes")]
{
crate::builtins::load_by_name("silkcircuit-neon")
.expect("default builtin theme must be valid")
}
#[cfg(not(feature = "builtin-themes"))]
{
Self {
meta: ThemeMeta {
name: "Fallback".to_string(),
author: None,
variant: ThemeVariant::Dark,
version: None,
description: None,
},
palette: HashMap::new(),
tokens: HashMap::new(),
styles: HashMap::new(),
#[cfg(feature = "gradients")]
gradients: HashMap::new(),
}
}
}
}
#[cfg(feature = "global-state")]
mod global {
use std::sync::{Arc, LazyLock};
use parking_lot::RwLock;
use super::Theme;
use crate::error::OpalineError;
#[cfg(feature = "discovery")]
fn load_from_dirs<I, P>(name: &str, dirs: I) -> Result<Option<Theme>, OpalineError>
where
I: IntoIterator<Item = P>,
P: Into<std::path::PathBuf>,
{
let mut matched_path = None;
for dir in dirs.into_iter().map(Into::into) {
let path = dir.join(format!("{name}.toml"));
if path.exists() {
matched_path = Some(path);
}
}
matched_path.map_or(Ok(None), |path| {
crate::loader::load_from_file(&path).map(Some)
})
}
static ACTIVE_THEME: LazyLock<RwLock<Arc<Theme>>> =
LazyLock::new(|| RwLock::new(Arc::new(Theme::default())));
pub fn current() -> Arc<Theme> {
ACTIVE_THEME.read().clone()
}
pub fn set_theme(theme: Theme) {
*ACTIVE_THEME.write() = Arc::new(theme);
}
#[cfg(feature = "builtin-themes")]
pub fn load_theme_by_name(name: &str) -> Result<(), OpalineError> {
#[cfg(feature = "discovery")]
return load_theme_by_name_in_dirs(name, crate::discovery::theme_dirs());
#[cfg(not(feature = "discovery"))]
{
if let Some(theme) = crate::builtins::load_by_name(name) {
set_theme(theme);
return Ok(());
}
Err(OpalineError::ThemeNotFound {
name: name.to_string(),
})
}
}
#[cfg(feature = "builtin-themes")]
pub fn load_theme_by_name_with<F>(name: &str, derive: F) -> Result<(), OpalineError>
where
F: FnOnce(&mut Theme),
{
#[cfg(feature = "discovery")]
if let Some(mut theme) = load_from_dirs(name, crate::discovery::theme_dirs())? {
derive(&mut theme);
set_theme(theme);
return Ok(());
}
if let Some(mut theme) = crate::builtins::load_by_name(name) {
derive(&mut theme);
set_theme(theme);
return Ok(());
}
Err(OpalineError::ThemeNotFound {
name: name.to_string(),
})
}
#[cfg(all(feature = "builtin-themes", feature = "discovery"))]
pub fn load_theme_by_name_for_app(name: &str, app_name: &str) -> Result<(), OpalineError> {
load_theme_by_name_in_dirs(name, crate::discovery::app_theme_dirs(app_name))
}
#[cfg(all(feature = "builtin-themes", feature = "discovery"))]
pub fn load_theme_by_name_in_dirs<I, P>(name: &str, dirs: I) -> Result<(), OpalineError>
where
I: IntoIterator<Item = P>,
P: Into<std::path::PathBuf>,
{
if let Some(theme) = load_from_dirs(name, dirs)? {
set_theme(theme);
return Ok(());
}
if let Some(theme) = crate::builtins::load_by_name(name) {
set_theme(theme);
return Ok(());
}
Err(OpalineError::ThemeNotFound {
name: name.to_string(),
})
}
#[cfg(all(feature = "builtin-themes", feature = "discovery"))]
pub fn load_theme_by_name_for_app_with<F>(
name: &str,
app_name: &str,
derive: F,
) -> Result<(), OpalineError>
where
F: FnOnce(&mut Theme),
{
if let Some(mut theme) = load_from_dirs(name, crate::discovery::app_theme_dirs(app_name))? {
derive(&mut theme);
set_theme(theme);
return Ok(());
}
if let Some(mut theme) = crate::builtins::load_by_name(name) {
derive(&mut theme);
set_theme(theme);
return Ok(());
}
Err(OpalineError::ThemeNotFound {
name: name.to_string(),
})
}
pub fn load_theme(path: &std::path::Path) -> Result<(), OpalineError> {
let theme = crate::loader::load_from_file(path)?;
set_theme(theme);
Ok(())
}
}
#[cfg(feature = "global-state")]
pub use global::{current, load_theme, set_theme};
#[cfg(all(feature = "global-state", feature = "builtin-themes"))]
pub use global::load_theme_by_name;
#[cfg(all(
feature = "global-state",
feature = "builtin-themes",
feature = "discovery"
))]
pub use global::load_theme_by_name_in_dirs;
#[cfg(all(
feature = "global-state",
feature = "builtin-themes",
feature = "discovery"
))]
pub use global::load_theme_by_name_for_app;
#[cfg(all(feature = "global-state", feature = "builtin-themes"))]
pub use global::load_theme_by_name_with;
#[cfg(all(
feature = "global-state",
feature = "builtin-themes",
feature = "discovery"
))]
pub use global::load_theme_by_name_for_app_with;