use super::properties::Color;
use crate::utils::lock::{read_or_recover, write_or_recover};
use crate::widget::theme::{EDITOR_BG, SECONDARY_TEXT};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ThemeVariant {
#[default]
Dark,
Light,
HighContrast,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Palette {
pub primary: Color,
pub secondary: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
}
impl Default for Palette {
fn default() -> Self {
Self::dark()
}
}
impl Palette {
pub fn dark() -> Self {
Self {
primary: Color::rgb(66, 133, 244), secondary: Color::rgb(156, 39, 176), success: Color::rgb(76, 175, 80), warning: Color::rgb(255, 193, 7), error: Color::rgb(244, 67, 54), info: Color::rgb(33, 150, 243), }
}
pub fn light() -> Self {
Self {
primary: Color::rgb(25, 118, 210), secondary: Color::rgb(123, 31, 162), success: Color::rgb(56, 142, 60), warning: Color::rgb(255, 160, 0), error: Color::rgb(211, 47, 47), info: Color::rgb(2, 136, 209), }
}
pub fn high_contrast() -> Self {
Self {
primary: Color::CYAN,
secondary: Color::MAGENTA,
success: Color::GREEN,
warning: Color::YELLOW,
error: Color::RED,
info: Color::BLUE,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ThemeColors {
pub background: Color,
pub surface: Color,
pub text: Color,
pub text_muted: Color,
pub border: Color,
pub divider: Color,
pub selection: Color,
pub selection_text: Color,
pub focus: Color,
}
impl Default for ThemeColors {
fn default() -> Self {
Self::dark()
}
}
impl ThemeColors {
pub fn dark() -> Self {
Self {
background: Color::rgb(18, 18, 18),
surface: EDITOR_BG,
text: Color::rgb(255, 255, 255),
text_muted: Color::rgb(158, 158, 158),
border: Color::rgb(66, 66, 66),
divider: Color::rgb(48, 48, 48),
selection: Color::rgb(66, 133, 244),
selection_text: Color::WHITE,
focus: Color::rgb(66, 133, 244),
}
}
pub fn light() -> Self {
Self {
background: Color::rgb(255, 255, 255),
surface: Color::rgb(250, 250, 250),
text: Color::rgb(33, 33, 33),
text_muted: Color::rgb(117, 117, 117),
border: Color::rgb(224, 224, 224),
divider: Color::rgb(238, 238, 238),
selection: Color::rgb(25, 118, 210),
selection_text: Color::WHITE,
focus: Color::rgb(25, 118, 210),
}
}
pub fn high_contrast() -> Self {
Self {
background: Color::BLACK,
surface: Color::BLACK,
text: Color::WHITE,
text_muted: SECONDARY_TEXT,
border: Color::WHITE,
divider: Color::WHITE,
selection: Color::YELLOW,
selection_text: Color::BLACK,
focus: Color::CYAN,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub name: String,
pub variant: ThemeVariant,
pub palette: Palette,
pub colors: ThemeColors,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn dark() -> Self {
Self {
name: "Dark".to_string(),
variant: ThemeVariant::Dark,
palette: Palette::dark(),
colors: ThemeColors::dark(),
}
}
pub fn light() -> Self {
Self {
name: "Light".to_string(),
variant: ThemeVariant::Light,
palette: Palette::light(),
colors: ThemeColors::light(),
}
}
pub fn high_contrast() -> Self {
Self {
name: "High Contrast".to_string(),
variant: ThemeVariant::HighContrast,
palette: Palette::high_contrast(),
colors: ThemeColors::high_contrast(),
}
}
pub fn custom(name: impl Into<String>) -> ThemeBuilder {
ThemeBuilder::new(name)
}
pub fn is_dark(&self) -> bool {
self.variant == ThemeVariant::Dark
}
pub fn is_light(&self) -> bool {
self.variant == ThemeVariant::Light
}
}
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
theme: Theme {
name: name.into(),
..Theme::dark()
},
}
}
pub fn variant(mut self, variant: ThemeVariant) -> Self {
self.theme.variant = variant;
self
}
pub fn palette(mut self, palette: Palette) -> Self {
self.theme.palette = palette;
self
}
pub fn colors(mut self, colors: ThemeColors) -> Self {
self.theme.colors = colors;
self
}
pub fn primary(mut self, color: Color) -> Self {
self.theme.palette.primary = color;
self
}
pub fn background(mut self, color: Color) -> Self {
self.theme.colors.background = color;
self
}
pub fn text(mut self, color: Color) -> Self {
self.theme.colors.text = color;
self
}
pub fn build(self) -> Theme {
self.theme
}
}
pub struct Themes;
impl Themes {
pub fn dracula() -> Theme {
Theme {
name: "Dracula".to_string(),
variant: ThemeVariant::Dark,
palette: Palette {
primary: Color::rgb(139, 233, 253), secondary: Color::rgb(255, 121, 198), success: Color::rgb(80, 250, 123), warning: Color::rgb(241, 250, 140), error: Color::rgb(255, 85, 85), info: Color::rgb(189, 147, 249), },
colors: ThemeColors {
background: Color::rgb(40, 42, 54),
surface: Color::rgb(68, 71, 90),
text: Color::rgb(248, 248, 242),
text_muted: Color::rgb(98, 114, 164),
border: Color::rgb(68, 71, 90),
divider: Color::rgb(68, 71, 90),
selection: Color::rgb(68, 71, 90),
selection_text: Color::rgb(248, 248, 242),
focus: Color::rgb(139, 233, 253),
},
}
}
pub fn nord() -> Theme {
Theme {
name: "Nord".to_string(),
variant: ThemeVariant::Dark,
palette: Palette {
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), },
colors: ThemeColors {
background: Color::rgb(46, 52, 64),
surface: Color::rgb(59, 66, 82),
text: Color::rgb(236, 239, 244),
text_muted: Color::rgb(147, 161, 161),
border: Color::rgb(76, 86, 106),
divider: Color::rgb(67, 76, 94),
selection: Color::rgb(76, 86, 106),
selection_text: Color::rgb(236, 239, 244),
focus: Color::rgb(136, 192, 208),
},
}
}
pub fn monokai() -> Theme {
Theme {
name: "Monokai".to_string(),
variant: ThemeVariant::Dark,
palette: Palette {
primary: Color::rgb(102, 217, 239), secondary: Color::rgb(174, 129, 255), success: Color::rgb(166, 226, 46), warning: Color::rgb(253, 151, 31), error: Color::rgb(249, 38, 114), info: Color::rgb(102, 217, 239), },
colors: ThemeColors {
background: Color::rgb(39, 40, 34),
surface: Color::rgb(49, 50, 44),
text: Color::rgb(248, 248, 242),
text_muted: Color::rgb(117, 113, 94),
border: Color::rgb(73, 72, 62),
divider: Color::rgb(73, 72, 62),
selection: Color::rgb(73, 72, 62),
selection_text: Color::rgb(248, 248, 242),
focus: Color::rgb(166, 226, 46),
},
}
}
pub fn solarized_dark() -> Theme {
Theme {
name: "Solarized Dark".to_string(),
variant: ThemeVariant::Dark,
palette: Palette {
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), },
colors: ThemeColors {
background: Color::rgb(0, 43, 54),
surface: Color::rgb(7, 54, 66),
text: Color::rgb(131, 148, 150),
text_muted: Color::rgb(88, 110, 117),
border: Color::rgb(7, 54, 66),
divider: Color::rgb(7, 54, 66),
selection: Color::rgb(7, 54, 66),
selection_text: Color::rgb(147, 161, 161),
focus: Color::rgb(38, 139, 210),
},
}
}
pub fn solarized_light() -> Theme {
Theme {
name: "Solarized Light".to_string(),
variant: ThemeVariant::Light,
palette: Palette {
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), },
colors: ThemeColors {
background: Color::rgb(253, 246, 227),
surface: Color::rgb(238, 232, 213),
text: Color::rgb(101, 123, 131),
text_muted: Color::rgb(147, 161, 161),
border: Color::rgb(238, 232, 213),
divider: Color::rgb(238, 232, 213),
selection: Color::rgb(238, 232, 213),
selection_text: Color::rgb(88, 110, 117),
focus: Color::rgb(38, 139, 210),
},
}
}
}
pub type ThemeChangeListener = Box<dyn Fn(&Theme) + Send + Sync>;
pub struct ThemeManager {
themes: HashMap<String, Theme>,
current_id: String,
listeners: Vec<ThemeChangeListener>,
light_theme: String,
dark_theme: String,
}
impl ThemeManager {
pub fn new() -> Self {
let mut manager = Self {
themes: HashMap::new(),
current_id: "dark".to_string(),
listeners: Vec::new(),
light_theme: "light".to_string(),
dark_theme: "dark".to_string(),
};
manager.register("dark", Theme::dark());
manager.register("light", Theme::light());
manager.register("high_contrast", Theme::high_contrast());
manager.register("dracula", Themes::dracula());
manager.register("nord", Themes::nord());
manager.register("monokai", Themes::monokai());
manager.register("solarized_dark", Themes::solarized_dark());
manager.register("solarized_light", Themes::solarized_light());
manager
}
pub fn with_theme(theme_id: impl Into<String>) -> Self {
let mut manager = Self::new();
let id = theme_id.into();
if manager.themes.contains_key(&id) {
manager.current_id = id;
}
manager
}
pub fn register(&mut self, id: impl Into<String>, theme: Theme) {
self.themes.insert(id.into(), theme);
}
pub fn unregister(&mut self, id: &str) -> Option<Theme> {
if id == self.current_id {
return None;
}
self.themes.remove(id)
}
pub fn set_theme(&mut self, id: impl Into<String>) -> bool {
let id = id.into();
if self.themes.contains_key(&id) {
self.current_id = id;
self.notify_change();
true
} else {
false
}
}
pub fn current(&self) -> &Theme {
self.themes.get(&self.current_id).unwrap_or_else(|| {
static DEFAULT: std::sync::OnceLock<Theme> = std::sync::OnceLock::new();
DEFAULT.get_or_init(Theme::dark)
})
}
pub fn current_id(&self) -> &str {
&self.current_id
}
pub fn get(&self, id: &str) -> Option<&Theme> {
self.themes.get(id)
}
pub fn theme_ids(&self) -> Vec<&str> {
self.themes.keys().map(|s| s.as_str()).collect()
}
pub fn themes(&self) -> impl Iterator<Item = (&str, &Theme)> {
self.themes.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn set_light_theme(&mut self, id: impl Into<String>) {
self.light_theme = id.into();
}
pub fn set_dark_theme(&mut self, id: impl Into<String>) {
self.dark_theme = id.into();
}
pub fn toggle_dark_light(&mut self) {
let new_id = if self.current().is_dark() {
self.light_theme.clone()
} else {
self.dark_theme.clone()
};
self.set_theme(new_id);
}
pub fn cycle(&mut self) {
let ids: Vec<String> = self.themes.keys().cloned().collect();
if ids.is_empty() {
return;
}
let current_idx = ids
.iter()
.position(|id| id == &self.current_id)
.unwrap_or(0);
let next_idx = (current_idx + 1) % ids.len();
self.set_theme(&ids[next_idx]);
}
pub fn cycle_dark(&mut self) {
let dark_ids: Vec<String> = self
.themes
.iter()
.filter(|(_, t)| t.is_dark())
.map(|(id, _)| id.clone())
.collect();
if dark_ids.is_empty() {
return;
}
let current_idx = dark_ids
.iter()
.position(|id| id == &self.current_id)
.unwrap_or(0);
let next_idx = (current_idx + 1) % dark_ids.len();
self.set_theme(&dark_ids[next_idx]);
}
pub fn cycle_light(&mut self) {
let light_ids: Vec<String> = self
.themes
.iter()
.filter(|(_, t)| t.is_light())
.map(|(id, _)| id.clone())
.collect();
if light_ids.is_empty() {
return;
}
let current_idx = light_ids
.iter()
.position(|id| id == &self.current_id)
.unwrap_or(0);
let next_idx = (current_idx + 1) % light_ids.len();
self.set_theme(&light_ids[next_idx]);
}
pub fn on_change<F>(&mut self, listener: F)
where
F: Fn(&Theme) + Send + Sync + 'static,
{
self.listeners.push(Box::new(listener));
}
fn notify_change(&self) {
let theme = self.current();
for listener in &self.listeners {
listener(theme);
}
}
pub fn has_theme(&self, id: &str) -> bool {
self.themes.contains_key(id)
}
pub fn len(&self) -> usize {
self.themes.len()
}
pub fn is_empty(&self) -> bool {
self.themes.is_empty()
}
}
impl Default for ThemeManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct SharedTheme {
inner: Arc<RwLock<ThemeManager>>,
}
impl SharedTheme {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(ThemeManager::new())),
}
}
pub fn with_theme(id: impl Into<String>) -> Self {
Self {
inner: Arc::new(RwLock::new(ThemeManager::with_theme(id))),
}
}
pub fn current(&self) -> Theme {
read_or_recover(&self.inner).current().clone()
}
pub fn current_id(&self) -> String {
read_or_recover(&self.inner).current_id().to_string()
}
pub fn set_theme(&self, id: impl Into<String>) -> bool {
write_or_recover(&self.inner).set_theme(id)
}
pub fn toggle_dark_light(&self) {
write_or_recover(&self.inner).toggle_dark_light();
}
pub fn cycle(&self) {
write_or_recover(&self.inner).cycle();
}
pub fn register(&self, id: impl Into<String>, theme: Theme) {
write_or_recover(&self.inner).register(id, theme);
}
pub fn theme_ids(&self) -> Vec<String> {
read_or_recover(&self.inner)
.theme_ids()
.into_iter()
.map(|s| s.to_string())
.collect()
}
}
impl Default for SharedTheme {
fn default() -> Self {
Self::new()
}
}
pub fn theme_manager() -> ThemeManager {
ThemeManager::new()
}
pub fn shared_theme() -> SharedTheme {
SharedTheme::new()
}
#[cfg(test)]
mod tests;