1use iced::Color;
2
3#[derive(Debug, Clone, Copy)]
5pub struct Style {
6 pub background: Color,
8 pub text_color: Color,
10 pub gutter_background: Color,
12 pub gutter_border: Color,
14 pub line_number_color: Color,
16 pub scrollbar_background: Color,
18 pub scroller_color: Color,
20 pub current_line_highlight: Color,
22 pub whitespace_color: Color,
24}
25
26pub trait Catalog {
28 type Class<'a>;
30
31 fn default<'a>() -> Self::Class<'a>;
33
34 fn style(&self, class: &Self::Class<'_>) -> Style;
36}
37
38pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
43
44impl Catalog for iced::Theme {
45 type Class<'a> = StyleFn<'a, Self>;
46
47 fn default<'a>() -> Self::Class<'a> {
48 Box::new(from_iced_theme)
49 }
50
51 fn style(&self, class: &Self::Class<'_>) -> Style {
52 class(self)
53 }
54}
55
56pub fn from_iced_theme(theme: &iced::Theme) -> Style {
93 let palette = theme.extended_palette();
94 let is_dark = palette.is_dark;
95
96 let background = palette.background.base.color;
98 let text_color = palette.background.base.text;
99
100 let gutter_background = palette.background.weak.color;
102 let gutter_border = if is_dark {
103 darken(palette.background.strong.color, 0.1)
104 } else {
105 lighten(palette.background.strong.color, 0.1)
106 };
107
108 let line_number_color = if is_dark {
112 dim_color(text_color, 0.5)
113 } else {
114 blend_colors(text_color, background, 0.5)
116 };
117
118 let scrollbar_background = background;
120 let scroller_color = palette.secondary.weak.color;
121
122 let current_line_highlight = with_alpha(
124 palette.primary.weak.color,
125 if is_dark { 0.15 } else { 0.25 },
126 );
127
128 let whitespace_color = if is_dark {
129 dim_color(text_color, 0.65)
130 } else {
131 blend_colors(text_color, background, 0.65)
132 };
133
134 Style {
135 background,
136 text_color,
137 gutter_background,
138 gutter_border,
139 line_number_color,
140 scrollbar_background,
141 scroller_color,
142 current_line_highlight,
143 whitespace_color,
144 }
145}
146
147fn darken(color: Color, factor: f32) -> Color {
149 Color {
150 r: color.r * (1.0 - factor),
151 g: color.g * (1.0 - factor),
152 b: color.b * (1.0 - factor),
153 a: color.a,
154 }
155}
156
157fn lighten(color: Color, factor: f32) -> Color {
159 Color {
160 r: color.r + (1.0 - color.r) * factor,
161 g: color.g + (1.0 - color.g) * factor,
162 b: color.b + (1.0 - color.b) * factor,
163 a: color.a,
164 }
165}
166
167fn dim_color(color: Color, factor: f32) -> Color {
169 Color {
170 r: color.r * factor,
171 g: color.g * factor,
172 b: color.b * factor,
173 a: color.a,
174 }
175}
176
177fn blend_colors(color1: Color, color2: Color, factor: f32) -> Color {
179 Color {
180 r: color1.r + (color2.r - color1.r) * factor,
181 g: color1.g + (color2.g - color1.g) * factor,
182 b: color1.b + (color2.b - color1.b) * factor,
183 a: color1.a + (color2.a - color1.a) * factor,
184 }
185}
186
187fn with_alpha(color: Color, alpha: f32) -> Color {
189 Color { r: color.r, g: color.g, b: color.b, a: alpha }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_from_iced_theme_dark() {
198 let theme = iced::Theme::Dark;
199 let style = from_iced_theme(&theme);
200
201 let brightness =
203 (style.background.r + style.background.g + style.background.b)
204 / 3.0;
205 assert!(brightness < 0.5, "Dark theme should have dark background");
206
207 let text_brightness =
209 (style.text_color.r + style.text_color.g + style.text_color.b)
210 / 3.0;
211 assert!(text_brightness > 0.5, "Dark theme should have bright text");
212 }
213
214 #[test]
215 fn test_from_iced_theme_light() {
216 let theme = iced::Theme::Light;
217 let style = from_iced_theme(&theme);
218
219 let brightness =
221 (style.background.r + style.background.g + style.background.b)
222 / 3.0;
223 assert!(brightness > 0.5, "Light theme should have bright background");
224
225 let text_brightness =
227 (style.text_color.r + style.text_color.g + style.text_color.b)
228 / 3.0;
229 assert!(text_brightness < 0.5, "Light theme should have dark text");
230 }
231
232 #[test]
233 fn test_all_iced_themes_produce_valid_styles() {
234 for theme in iced::Theme::ALL {
236 let style = from_iced_theme(theme);
237
238 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
240 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
241 assert!(
242 style.gutter_background.r >= 0.0
243 && style.gutter_background.r <= 1.0
244 );
245 assert!(
246 style.line_number_color.r >= 0.0
247 && style.line_number_color.r <= 1.0
248 );
249
250 assert!(
252 style.current_line_highlight.a < 1.0,
253 "Current line highlight should be semi-transparent for theme: {:?}",
254 theme
255 );
256 }
257 }
258
259 #[test]
260 fn test_tokyo_night_themes() {
261 let tokyo_night = iced::Theme::TokyoNight;
263 let style = from_iced_theme(&tokyo_night);
264 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
265
266 let tokyo_storm = iced::Theme::TokyoNightStorm;
267 let style = from_iced_theme(&tokyo_storm);
268 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
269
270 let tokyo_light = iced::Theme::TokyoNightLight;
271 let style = from_iced_theme(&tokyo_light);
272 let brightness =
273 (style.background.r + style.background.g + style.background.b)
274 / 3.0;
275 assert!(
276 brightness > 0.5,
277 "Tokyo Night Light should have bright background"
278 );
279 }
280
281 #[test]
282 fn test_catppuccin_themes() {
283 let themes = [
285 iced::Theme::CatppuccinLatte,
286 iced::Theme::CatppuccinFrappe,
287 iced::Theme::CatppuccinMacchiato,
288 iced::Theme::CatppuccinMocha,
289 ];
290
291 for theme in themes {
292 let style = from_iced_theme(&theme);
293 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
295 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
296 }
297 }
298
299 #[test]
300 fn test_gutter_colors_distinct_from_background() {
301 let theme = iced::Theme::Dark;
302 let style = from_iced_theme(&theme);
303
304 let gutter_diff = (style.gutter_background.r - style.background.r)
306 .abs()
307 + (style.gutter_background.g - style.background.g).abs()
308 + (style.gutter_background.b - style.background.b).abs();
309
310 assert!(
311 gutter_diff > 0.0,
312 "Gutter should be visually distinct from background"
313 );
314 }
315
316 #[test]
317 fn test_line_numbers_visible_but_subtle() {
318 for theme in [iced::Theme::Dark, iced::Theme::Light] {
319 let style = from_iced_theme(&theme);
320 let palette = theme.extended_palette();
321
322 let line_num_brightness = (style.line_number_color.r
324 + style.line_number_color.g
325 + style.line_number_color.b)
326 / 3.0;
327
328 let text_brightness =
329 (style.text_color.r + style.text_color.g + style.text_color.b)
330 / 3.0;
331
332 let bg_brightness =
333 (style.background.r + style.background.g + style.background.b)
334 / 3.0;
335
336 if palette.is_dark {
340 assert!(
342 line_num_brightness < text_brightness,
343 "Dark theme line numbers should be dimmer than text. Line num: {}, Text: {}",
344 line_num_brightness,
345 text_brightness
346 );
347 } else {
348 assert!(
350 line_num_brightness > text_brightness
351 && line_num_brightness < bg_brightness,
352 "Light theme line numbers should be between text and background. Text: {}, Line num: {}, Bg: {}",
353 text_brightness,
354 line_num_brightness,
355 bg_brightness
356 );
357 }
358 }
359 }
360
361 #[test]
362 fn test_color_helper_functions() {
363 let color = Color::from_rgb(0.5, 0.5, 0.5);
364
365 let darker = darken(color, 0.5);
367 assert!(darker.r < color.r);
368 assert!(darker.g < color.g);
369 assert!(darker.b < color.b);
370
371 let lighter = lighten(color, 0.5);
373 assert!(lighter.r > color.r);
374 assert!(lighter.g > color.g);
375 assert!(lighter.b > color.b);
376
377 let dimmed = dim_color(color, 0.5);
379 assert!(dimmed.r < color.r);
380
381 let transparent = with_alpha(color, 0.3);
383 assert!((transparent.a - 0.3).abs() < f32::EPSILON);
384 assert!((transparent.r - color.r).abs() < f32::EPSILON);
385 }
386
387 #[test]
388 fn test_style_copy() {
389 let theme = iced::Theme::Dark;
390 let style1 = from_iced_theme(&theme);
391 let style2 = style1;
392
393 assert!(
395 (style1.background.r - style2.background.r).abs() < f32::EPSILON
396 );
397 assert!(
398 (style1.text_color.r - style2.text_color.r).abs() < f32::EPSILON
399 );
400 assert!(
401 (style1.gutter_background.r - style2.gutter_background.r).abs()
402 < f32::EPSILON
403 );
404 }
405
406 #[test]
407 fn test_catalog_default() {
408 let theme = iced::Theme::Dark;
409 let class = <iced::Theme as Catalog>::default();
410 let style = theme.style(&class);
411
412 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
414 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
415 }
416}