use crate::core::Color;
mod tokens;
pub use tokens::{
BorderTokens, ComponentState, ComponentVariant, Density, DensityTokens, DesignTokens,
FocusTokens, SpacingTokens, StateTokens, SymbolTokens, VariantStyle,
};
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
pub primary: Color,
pub secondary: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub text: TextColors,
pub background: BackgroundColors,
pub border: BorderColors,
pub components: ComponentColors,
}
#[derive(Debug, Clone)]
pub struct TextColors {
pub primary: Color,
pub secondary: Color,
pub disabled: Color,
pub inverted: Color,
pub link: Color,
}
#[derive(Debug, Clone)]
pub struct BackgroundColors {
pub default: Color,
pub elevated: Color,
pub selected: Color,
pub hover: Color,
pub disabled: Color,
}
#[derive(Debug, Clone)]
pub struct BorderColors {
pub default: Color,
pub focused: Color,
pub error: Color,
pub disabled: Color,
}
#[derive(Debug, Clone)]
pub struct ComponentColors {
pub input: InputColors,
pub button: ButtonColors,
pub list: ListColors,
pub progress: ProgressColors,
}
#[derive(Debug, Clone)]
pub struct InputColors {
pub background: Color,
pub text: Color,
pub placeholder: Color,
pub cursor: Color,
pub selection: Color,
}
#[derive(Debug, Clone)]
pub struct ButtonColors {
pub primary_bg: Color,
pub primary_text: Color,
pub secondary_bg: Color,
pub secondary_text: Color,
pub danger_bg: Color,
pub danger_text: Color,
}
#[derive(Debug, Clone)]
pub struct ListColors {
pub item_bg: Color,
pub item_text: Color,
pub selected_bg: Color,
pub selected_text: Color,
pub focused_bg: Color,
pub focused_text: Color,
}
#[derive(Debug, Clone)]
pub struct ProgressColors {
pub track: Color,
pub fill: Color,
pub completed: Color,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn builder(name: impl Into<String>) -> ThemeBuilder {
ThemeBuilder::new(name)
}
pub fn dark() -> Self {
Self {
name: "dark".to_string(),
primary: Color::Cyan,
secondary: Color::Magenta,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Blue,
text: TextColors {
primary: Color::White,
secondary: Color::BrightBlack,
disabled: Color::BrightBlack,
inverted: Color::Black,
link: Color::Cyan,
},
background: BackgroundColors {
default: Color::Black,
elevated: Color::BrightBlack,
selected: Color::Blue,
hover: Color::BrightBlack,
disabled: Color::BrightBlack,
},
border: BorderColors {
default: Color::BrightBlack,
focused: Color::Cyan,
error: Color::Red,
disabled: Color::BrightBlack,
},
components: ComponentColors {
input: InputColors {
background: Color::Black,
text: Color::White,
placeholder: Color::BrightBlack,
cursor: Color::Cyan,
selection: Color::Blue,
},
button: ButtonColors {
primary_bg: Color::Cyan,
primary_text: Color::Black,
secondary_bg: Color::BrightBlack,
secondary_text: Color::White,
danger_bg: Color::Red,
danger_text: Color::White,
},
list: ListColors {
item_bg: Color::Black,
item_text: Color::White,
selected_bg: Color::Blue,
selected_text: Color::White,
focused_bg: Color::Cyan,
focused_text: Color::Black,
},
progress: ProgressColors {
track: Color::BrightBlack,
fill: Color::Cyan,
completed: Color::Green,
},
},
}
}
pub fn light() -> Self {
Self {
name: "light".to_string(),
primary: Color::Blue,
secondary: Color::Magenta,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Cyan,
text: TextColors {
primary: Color::Black,
secondary: Color::BrightBlack,
disabled: Color::BrightBlack,
inverted: Color::White,
link: Color::Blue,
},
background: BackgroundColors {
default: Color::White,
elevated: Color::BrightWhite,
selected: Color::Cyan,
hover: Color::BrightWhite,
disabled: Color::BrightWhite,
},
border: BorderColors {
default: Color::BrightBlack,
focused: Color::Blue,
error: Color::Red,
disabled: Color::BrightWhite,
},
components: ComponentColors {
input: InputColors {
background: Color::White,
text: Color::Black,
placeholder: Color::BrightBlack,
cursor: Color::Blue,
selection: Color::Cyan,
},
button: ButtonColors {
primary_bg: Color::Blue,
primary_text: Color::White,
secondary_bg: Color::BrightWhite,
secondary_text: Color::Black,
danger_bg: Color::Red,
danger_text: Color::White,
},
list: ListColors {
item_bg: Color::White,
item_text: Color::Black,
selected_bg: Color::Cyan,
selected_text: Color::Black,
focused_bg: Color::Blue,
focused_text: Color::White,
},
progress: ProgressColors {
track: Color::BrightWhite,
fill: Color::Blue,
completed: Color::Green,
},
},
}
}
pub fn monokai() -> Self {
Self {
name: "monokai".to_string(),
primary: Color::Rgb(166, 226, 46), secondary: Color::Rgb(174, 129, 255), success: Color::Rgb(166, 226, 46),
warning: Color::Rgb(230, 219, 116), error: Color::Rgb(249, 38, 114), info: Color::Rgb(102, 217, 239), text: TextColors {
primary: Color::Rgb(248, 248, 242),
secondary: Color::Rgb(117, 113, 94),
disabled: Color::Rgb(117, 113, 94),
inverted: Color::Rgb(39, 40, 34),
link: Color::Rgb(102, 217, 239),
},
background: BackgroundColors {
default: Color::Rgb(39, 40, 34),
elevated: Color::Rgb(49, 50, 44),
selected: Color::Rgb(73, 72, 62),
hover: Color::Rgb(59, 60, 54),
disabled: Color::Rgb(49, 50, 44),
},
border: BorderColors {
default: Color::Rgb(117, 113, 94),
focused: Color::Rgb(166, 226, 46),
error: Color::Rgb(249, 38, 114),
disabled: Color::Rgb(73, 72, 62),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(39, 40, 34),
text: Color::Rgb(248, 248, 242),
placeholder: Color::Rgb(117, 113, 94),
cursor: Color::Rgb(248, 248, 242),
selection: Color::Rgb(73, 72, 62),
},
button: ButtonColors {
primary_bg: Color::Rgb(166, 226, 46),
primary_text: Color::Rgb(39, 40, 34),
secondary_bg: Color::Rgb(73, 72, 62),
secondary_text: Color::Rgb(248, 248, 242),
danger_bg: Color::Rgb(249, 38, 114),
danger_text: Color::Rgb(248, 248, 242),
},
list: ListColors {
item_bg: Color::Rgb(39, 40, 34),
item_text: Color::Rgb(248, 248, 242),
selected_bg: Color::Rgb(73, 72, 62),
selected_text: Color::Rgb(248, 248, 242),
focused_bg: Color::Rgb(166, 226, 46),
focused_text: Color::Rgb(39, 40, 34),
},
progress: ProgressColors {
track: Color::Rgb(73, 72, 62),
fill: Color::Rgb(166, 226, 46),
completed: Color::Rgb(166, 226, 46),
},
},
}
}
pub fn dracula() -> Self {
Self {
name: "dracula".to_string(),
primary: Color::Rgb(189, 147, 249), secondary: Color::Rgb(255, 121, 198), success: Color::Rgb(80, 250, 123), warning: Color::Rgb(255, 184, 108), error: Color::Rgb(255, 85, 85), info: Color::Rgb(139, 233, 253), text: TextColors {
primary: Color::Rgb(248, 248, 242),
secondary: Color::Rgb(98, 114, 164),
disabled: Color::Rgb(68, 71, 90),
inverted: Color::Rgb(40, 42, 54),
link: Color::Rgb(139, 233, 253),
},
background: BackgroundColors {
default: Color::Rgb(40, 42, 54),
elevated: Color::Rgb(68, 71, 90),
selected: Color::Rgb(68, 71, 90),
hover: Color::Rgb(68, 71, 90),
disabled: Color::Rgb(68, 71, 90),
},
border: BorderColors {
default: Color::Rgb(68, 71, 90),
focused: Color::Rgb(189, 147, 249),
error: Color::Rgb(255, 85, 85),
disabled: Color::Rgb(68, 71, 90),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(40, 42, 54),
text: Color::Rgb(248, 248, 242),
placeholder: Color::Rgb(98, 114, 164),
cursor: Color::Rgb(248, 248, 242),
selection: Color::Rgb(68, 71, 90),
},
button: ButtonColors {
primary_bg: Color::Rgb(189, 147, 249),
primary_text: Color::Rgb(40, 42, 54),
secondary_bg: Color::Rgb(68, 71, 90),
secondary_text: Color::Rgb(248, 248, 242),
danger_bg: Color::Rgb(255, 85, 85),
danger_text: Color::Rgb(248, 248, 242),
},
list: ListColors {
item_bg: Color::Rgb(40, 42, 54),
item_text: Color::Rgb(248, 248, 242),
selected_bg: Color::Rgb(68, 71, 90),
selected_text: Color::Rgb(248, 248, 242),
focused_bg: Color::Rgb(189, 147, 249),
focused_text: Color::Rgb(40, 42, 54),
},
progress: ProgressColors {
track: Color::Rgb(68, 71, 90),
fill: Color::Rgb(189, 147, 249),
completed: Color::Rgb(80, 250, 123),
},
},
}
}
pub fn nord() -> Self {
Self {
name: "nord".to_string(),
primary: Color::Rgb(136, 192, 208), secondary: Color::Rgb(180, 142, 173), success: Color::Rgb(163, 190, 140), warning: Color::Rgb(235, 203, 139), error: Color::Rgb(191, 97, 106), info: Color::Rgb(129, 161, 193), text: TextColors {
primary: Color::Rgb(236, 239, 244),
secondary: Color::Rgb(76, 86, 106),
disabled: Color::Rgb(76, 86, 106),
inverted: Color::Rgb(46, 52, 64),
link: Color::Rgb(136, 192, 208),
},
background: BackgroundColors {
default: Color::Rgb(46, 52, 64),
elevated: Color::Rgb(59, 66, 82),
selected: Color::Rgb(67, 76, 94),
hover: Color::Rgb(59, 66, 82),
disabled: Color::Rgb(59, 66, 82),
},
border: BorderColors {
default: Color::Rgb(76, 86, 106),
focused: Color::Rgb(136, 192, 208),
error: Color::Rgb(191, 97, 106),
disabled: Color::Rgb(59, 66, 82),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(46, 52, 64),
text: Color::Rgb(236, 239, 244),
placeholder: Color::Rgb(76, 86, 106),
cursor: Color::Rgb(236, 239, 244),
selection: Color::Rgb(67, 76, 94),
},
button: ButtonColors {
primary_bg: Color::Rgb(136, 192, 208),
primary_text: Color::Rgb(46, 52, 64),
secondary_bg: Color::Rgb(67, 76, 94),
secondary_text: Color::Rgb(236, 239, 244),
danger_bg: Color::Rgb(191, 97, 106),
danger_text: Color::Rgb(236, 239, 244),
},
list: ListColors {
item_bg: Color::Rgb(46, 52, 64),
item_text: Color::Rgb(236, 239, 244),
selected_bg: Color::Rgb(67, 76, 94),
selected_text: Color::Rgb(236, 239, 244),
focused_bg: Color::Rgb(136, 192, 208),
focused_text: Color::Rgb(46, 52, 64),
},
progress: ProgressColors {
track: Color::Rgb(67, 76, 94),
fill: Color::Rgb(136, 192, 208),
completed: Color::Rgb(163, 190, 140),
},
},
}
}
pub fn solarized_dark() -> Self {
Self {
name: "solarized_dark".to_string(),
primary: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(181, 137, 0), error: Color::Rgb(220, 50, 47), info: Color::Rgb(42, 161, 152), text: TextColors {
primary: Color::Rgb(131, 148, 150),
secondary: Color::Rgb(88, 110, 117),
disabled: Color::Rgb(88, 110, 117),
inverted: Color::Rgb(0, 43, 54),
link: Color::Rgb(38, 139, 210),
},
background: BackgroundColors {
default: Color::Rgb(0, 43, 54),
elevated: Color::Rgb(7, 54, 66),
selected: Color::Rgb(7, 54, 66),
hover: Color::Rgb(7, 54, 66),
disabled: Color::Rgb(7, 54, 66),
},
border: BorderColors {
default: Color::Rgb(88, 110, 117),
focused: Color::Rgb(38, 139, 210),
error: Color::Rgb(220, 50, 47),
disabled: Color::Rgb(7, 54, 66),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(0, 43, 54),
text: Color::Rgb(131, 148, 150),
placeholder: Color::Rgb(88, 110, 117),
cursor: Color::Rgb(131, 148, 150),
selection: Color::Rgb(7, 54, 66),
},
button: ButtonColors {
primary_bg: Color::Rgb(38, 139, 210),
primary_text: Color::Rgb(253, 246, 227),
secondary_bg: Color::Rgb(7, 54, 66),
secondary_text: Color::Rgb(131, 148, 150),
danger_bg: Color::Rgb(220, 50, 47),
danger_text: Color::Rgb(253, 246, 227),
},
list: ListColors {
item_bg: Color::Rgb(0, 43, 54),
item_text: Color::Rgb(131, 148, 150),
selected_bg: Color::Rgb(7, 54, 66),
selected_text: Color::Rgb(131, 148, 150),
focused_bg: Color::Rgb(38, 139, 210),
focused_text: Color::Rgb(253, 246, 227),
},
progress: ProgressColors {
track: Color::Rgb(7, 54, 66),
fill: Color::Rgb(38, 139, 210),
completed: Color::Rgb(133, 153, 0),
},
},
}
}
pub fn by_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"dark" => Some(Self::dark()),
"light" => Some(Self::light()),
"monokai" => Some(Self::monokai()),
"dracula" => Some(Self::dracula()),
"nord" => Some(Self::nord()),
"solarized" | "solarized_dark" => Some(Self::solarized_dark()),
_ => None,
}
}
pub fn available_themes() -> Vec<&'static str> {
vec![
"dark",
"light",
"monokai",
"dracula",
"nord",
"solarized_dark",
]
}
pub fn semantic_color(&self, semantic: SemanticColor) -> Color {
match semantic {
SemanticColor::Primary => self.primary,
SemanticColor::Secondary => self.secondary,
SemanticColor::Success => self.success,
SemanticColor::Warning => self.warning,
SemanticColor::Error => self.error,
SemanticColor::Info => self.info,
SemanticColor::TextPrimary => self.text.primary,
SemanticColor::TextSecondary => self.text.secondary,
SemanticColor::TextDisabled => self.text.disabled,
SemanticColor::Background => self.background.default,
SemanticColor::BackgroundElevated => self.background.elevated,
SemanticColor::Border => self.border.default,
SemanticColor::BorderFocused => self.border.focused,
}
}
pub fn semantic_fg(&self, semantic: SemanticColor) -> String {
self.semantic_color(semantic).to_ansi_fg()
}
pub fn semantic_bg(&self, semantic: SemanticColor) -> String {
self.semantic_color(semantic).to_ansi_bg()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticColor {
Primary,
Secondary,
Success,
Warning,
Error,
Info,
TextPrimary,
TextSecondary,
TextDisabled,
Background,
BackgroundElevated,
Border,
BorderFocused,
}
#[derive(Debug, Clone)]
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn new(name: impl Into<String>) -> Self {
let mut theme = Theme::dark();
theme.name = name.into();
Self { theme }
}
pub fn primary(mut self, color: Color) -> Self {
self.theme.primary = color;
self
}
pub fn secondary(mut self, color: Color) -> Self {
self.theme.secondary = color;
self
}
pub fn success(mut self, color: Color) -> Self {
self.theme.success = color;
self
}
pub fn warning(mut self, color: Color) -> Self {
self.theme.warning = color;
self
}
pub fn error(mut self, color: Color) -> Self {
self.theme.error = color;
self
}
pub fn info(mut self, color: Color) -> Self {
self.theme.info = color;
self
}
pub fn text_colors(mut self, colors: TextColors) -> Self {
self.theme.text = colors;
self
}
pub fn background_colors(mut self, colors: BackgroundColors) -> Self {
self.theme.background = colors;
self
}
pub fn border_colors(mut self, colors: BorderColors) -> Self {
self.theme.border = colors;
self
}
pub fn build(self) -> Theme {
self.theme
}
}
thread_local! {
static CURRENT_THEME: std::cell::RefCell<Theme> = std::cell::RefCell::new(Theme::dark());
}
pub fn set_theme(theme: Theme) {
if let Some(ctx) = crate::runtime::current_runtime() {
ctx.borrow_mut().set_theme(theme);
return;
}
CURRENT_THEME.with(|t| {
*t.borrow_mut() = theme;
});
}
pub fn get_theme() -> Theme {
if let Some(ctx) = crate::runtime::current_runtime() {
return ctx.borrow().theme();
}
CURRENT_THEME.with(|t| t.borrow().clone())
}
pub fn with_theme<F, R>(theme: Theme, f: F) -> R
where
F: FnOnce(&Theme) -> R,
{
struct ThemeRestoreGuard {
previous_theme: Option<Theme>,
}
impl Drop for ThemeRestoreGuard {
fn drop(&mut self) {
if let Some(previous_theme) = self.previous_theme.take() {
set_theme(previous_theme);
}
}
}
let old_theme = get_theme();
set_theme(theme.clone());
let _guard = ThemeRestoreGuard {
previous_theme: Some(old_theme),
};
f(&theme)
}
#[cfg(test)]
mod tests;