1use std::fmt;
4
5use crate::Color;
6
7use super::{ColorScheme, SemanticTokens, Theme};
8
9#[derive(Clone, Copy, PartialEq, Eq, Hash)]
34pub struct ThemePreset(usize);
35
36impl ThemePreset {
37 pub const fn all() -> &'static [Self] {
39 &generated::THEME_PRESETS
40 }
41
42 pub fn get(name: &str) -> Option<Self> {
44 generated::THEME_DATA
45 .binary_search_by(|data| data.name.cmp(name))
46 .ok()
47 .map(Self)
48 }
49
50 pub const fn name(self) -> &'static str {
52 generated::THEME_DATA[self.0].name
53 }
54
55 pub const fn scheme(self) -> ColorScheme {
57 generated::THEME_DATA[self.0].scheme
58 }
59
60 pub fn theme(self) -> Theme {
62 generated::THEME_DATA[self.0].palette.theme()
63 }
64}
65
66impl fmt::Debug for ThemePreset {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter
69 .debug_struct("ThemePreset")
70 .field("name", &self.name())
71 .field("scheme", &self.scheme())
72 .finish()
73 }
74}
75
76impl fmt::Display for ThemePreset {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter.write_str(self.name())
79 }
80}
81
82#[derive(Clone, Copy)]
83struct ThemeData {
84 name: &'static str,
85 scheme: ColorScheme,
86 palette: PaletteTheme,
87}
88
89#[derive(Clone, Copy)]
90struct PaletteTheme {
91 foreground: Rgb,
92 background: Rgb,
93 red: Rgb,
94 green: Rgb,
95 yellow: Rgb,
96 blue: Rgb,
97}
98
99impl PaletteTheme {
100 fn theme(self) -> Theme {
101 Theme::from_tokens(SemanticTokens {
102 text: self.foreground.color(),
103 text_muted: mix(self.background, self.foreground, 55).color(),
104 background: self.background.color(),
105 surface: mix(self.background, self.foreground, 8).color(),
106 accent: self.blue.color(),
107 accent_text: self.background.color(),
108 success: self.green.color(),
109 warning: self.yellow.color(),
110 error: self.red.color(),
111 border: mix(self.background, self.foreground, 25).color(),
112 })
113 }
114}
115
116#[derive(Clone, Copy)]
117struct Rgb(u8, u8, u8);
118
119impl Rgb {
120 const fn color(self) -> Color {
121 Color::Rgb(self.0, self.1, self.2)
122 }
123}
124
125fn mix(background: Rgb, foreground: Rgb, foreground_percent: u16) -> Rgb {
126 fn channel(background: u8, foreground: u8, foreground_percent: u16) -> u8 {
127 let background_percent = 100 - foreground_percent;
128 let mixed =
129 u16::from(background) * background_percent + u16::from(foreground) * foreground_percent;
130 ((mixed + 50) / 100) as u8
131 }
132
133 Rgb(
134 channel(background.0, foreground.0, foreground_percent),
135 channel(background.1, foreground.1, foreground_percent),
136 channel(background.2, foreground.2, foreground_percent),
137 )
138}
139
140mod generated {
141 use super::{ColorScheme, PaletteTheme, Rgb, ThemeData, ThemePreset};
142
143 include!("generated_presets.rs");
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn catalog_contains_the_complete_pinned_snapshot() {
152 assert_eq!(ThemePreset::all().len(), 463);
153 assert_eq!(
154 ThemePreset::all()
155 .iter()
156 .filter(|preset| preset.scheme() == ColorScheme::Light)
157 .count(),
158 78
159 );
160 assert_eq!(
161 ThemePreset::all()
162 .iter()
163 .filter(|preset| preset.scheme() == ColorScheme::Dark)
164 .count(),
165 385
166 );
167 }
168
169 #[test]
170 fn catalog_names_are_sorted_and_unique() {
171 for presets in ThemePreset::all().windows(2) {
172 assert!(presets[0].name() < presets[1].name());
173 }
174 }
175
176 #[test]
177 fn exact_name_lookup_returns_the_catalog_value() {
178 let preset = ThemePreset::get("Catppuccin Mocha").expect("preset exists");
179 assert_eq!(preset.name(), "Catppuccin Mocha");
180 assert_eq!(preset.scheme(), ColorScheme::Dark);
181 assert_eq!(
182 preset.theme().tokens().background,
183 Color::Rgb(0x1e, 0x1e, 0x2e)
184 );
185 assert!(ThemePreset::get("catppuccin mocha").is_none());
186 }
187
188 #[test]
189 fn every_preset_builds_a_semantic_theme() {
190 for preset in ThemePreset::all() {
191 let tokens = preset.theme().tokens().to_owned();
192 assert_ne!(tokens.text, tokens.background, "{preset}");
193 assert_ne!(tokens.surface, tokens.background, "{preset}");
194 }
195 }
196}