iced_code_editor/
theme.rs1use 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}
23
24pub trait Catalog {
26 type Class<'a>;
28
29 fn default<'a>() -> Self::Class<'a>;
31
32 fn style(&self, class: &Self::Class<'_>) -> Style;
34}
35
36pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
41
42impl Catalog for iced::Theme {
43 type Class<'a> = StyleFn<'a, Self>;
44
45 fn default<'a>() -> Self::Class<'a> {
46 Box::new(from_iced_theme)
47 }
48
49 fn style(&self, class: &Self::Class<'_>) -> Style {
50 class(self)
51 }
52}
53
54pub fn from_iced_theme(theme: &iced::Theme) -> Style {
91 let palette = theme.extended_palette();
92 let is_dark = palette.is_dark;
93
94 let background = palette.background.base.color;
96 let text_color = palette.background.base.text;
97
98 let gutter_background = palette.background.weak.color;
100 let gutter_border = if is_dark {
101 darken(palette.background.strong.color, 0.1)
102 } else {
103 lighten(palette.background.strong.color, 0.1)
104 };
105
106 let line_number_color = if is_dark {
110 dim_color(text_color, 0.5)
111 } else {
112 blend_colors(text_color, background, 0.5)
114 };
115
116 let scrollbar_background = background;
118 let scroller_color = palette.secondary.weak.color;
119
120 let current_line_highlight = with_alpha(
122 palette.primary.weak.color,
123 if is_dark { 0.15 } else { 0.25 },
124 );
125
126 Style {
127 background,
128 text_color,
129 gutter_background,
130 gutter_border,
131 line_number_color,
132 scrollbar_background,
133 scroller_color,
134 current_line_highlight,
135 }
136}
137
138fn darken(color: Color, factor: f32) -> Color {
140 Color {
141 r: color.r * (1.0 - factor),
142 g: color.g * (1.0 - factor),
143 b: color.b * (1.0 - factor),
144 a: color.a,
145 }
146}
147
148fn lighten(color: Color, factor: f32) -> Color {
150 Color {
151 r: color.r + (1.0 - color.r) * factor,
152 g: color.g + (1.0 - color.g) * factor,
153 b: color.b + (1.0 - color.b) * factor,
154 a: color.a,
155 }
156}
157
158fn dim_color(color: Color, factor: f32) -> Color {
160 Color {
161 r: color.r * factor,
162 g: color.g * factor,
163 b: color.b * factor,
164 a: color.a,
165 }
166}
167
168fn blend_colors(color1: Color, color2: Color, factor: f32) -> Color {
170 Color {
171 r: color1.r + (color2.r - color1.r) * factor,
172 g: color1.g + (color2.g - color1.g) * factor,
173 b: color1.b + (color2.b - color1.b) * factor,
174 a: color1.a + (color2.a - color1.a) * factor,
175 }
176}
177
178fn with_alpha(color: Color, alpha: f32) -> Color {
180 Color { r: color.r, g: color.g, b: color.b, a: alpha }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_from_iced_theme_dark() {
189 let theme = iced::Theme::Dark;
190 let style = from_iced_theme(&theme);
191
192 let brightness =
194 (style.background.r + style.background.g + style.background.b)
195 / 3.0;
196 assert!(brightness < 0.5, "Dark theme should have dark background");
197
198 let text_brightness =
200 (style.text_color.r + style.text_color.g + style.text_color.b)
201 / 3.0;
202 assert!(text_brightness > 0.5, "Dark theme should have bright text");
203 }
204
205 #[test]
206 fn test_from_iced_theme_light() {
207 let theme = iced::Theme::Light;
208 let style = from_iced_theme(&theme);
209
210 let brightness =
212 (style.background.r + style.background.g + style.background.b)
213 / 3.0;
214 assert!(brightness > 0.5, "Light theme should have bright background");
215
216 let text_brightness =
218 (style.text_color.r + style.text_color.g + style.text_color.b)
219 / 3.0;
220 assert!(text_brightness < 0.5, "Light theme should have dark text");
221 }
222
223 #[test]
224 fn test_all_iced_themes_produce_valid_styles() {
225 for theme in iced::Theme::ALL {
227 let style = from_iced_theme(theme);
228
229 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
231 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
232 assert!(
233 style.gutter_background.r >= 0.0
234 && style.gutter_background.r <= 1.0
235 );
236 assert!(
237 style.line_number_color.r >= 0.0
238 && style.line_number_color.r <= 1.0
239 );
240
241 assert!(
243 style.current_line_highlight.a < 1.0,
244 "Current line highlight should be semi-transparent for theme: {:?}",
245 theme
246 );
247 }
248 }
249
250 #[test]
251 fn test_tokyo_night_themes() {
252 let tokyo_night = iced::Theme::TokyoNight;
254 let style = from_iced_theme(&tokyo_night);
255 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
256
257 let tokyo_storm = iced::Theme::TokyoNightStorm;
258 let style = from_iced_theme(&tokyo_storm);
259 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
260
261 let tokyo_light = iced::Theme::TokyoNightLight;
262 let style = from_iced_theme(&tokyo_light);
263 let brightness =
264 (style.background.r + style.background.g + style.background.b)
265 / 3.0;
266 assert!(
267 brightness > 0.5,
268 "Tokyo Night Light should have bright background"
269 );
270 }
271
272 #[test]
273 fn test_catppuccin_themes() {
274 let themes = [
276 iced::Theme::CatppuccinLatte,
277 iced::Theme::CatppuccinFrappe,
278 iced::Theme::CatppuccinMacchiato,
279 iced::Theme::CatppuccinMocha,
280 ];
281
282 for theme in themes {
283 let style = from_iced_theme(&theme);
284 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
286 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
287 }
288 }
289
290 #[test]
291 fn test_gutter_colors_distinct_from_background() {
292 let theme = iced::Theme::Dark;
293 let style = from_iced_theme(&theme);
294
295 let gutter_diff = (style.gutter_background.r - style.background.r)
297 .abs()
298 + (style.gutter_background.g - style.background.g).abs()
299 + (style.gutter_background.b - style.background.b).abs();
300
301 assert!(
302 gutter_diff > 0.0,
303 "Gutter should be visually distinct from background"
304 );
305 }
306
307 #[test]
308 fn test_line_numbers_visible_but_subtle() {
309 for theme in [iced::Theme::Dark, iced::Theme::Light] {
310 let style = from_iced_theme(&theme);
311 let palette = theme.extended_palette();
312
313 let line_num_brightness = (style.line_number_color.r
315 + style.line_number_color.g
316 + style.line_number_color.b)
317 / 3.0;
318
319 let text_brightness =
320 (style.text_color.r + style.text_color.g + style.text_color.b)
321 / 3.0;
322
323 let bg_brightness =
324 (style.background.r + style.background.g + style.background.b)
325 / 3.0;
326
327 if palette.is_dark {
331 assert!(
333 line_num_brightness < text_brightness,
334 "Dark theme line numbers should be dimmer than text. Line num: {}, Text: {}",
335 line_num_brightness,
336 text_brightness
337 );
338 } else {
339 assert!(
341 line_num_brightness > text_brightness
342 && line_num_brightness < bg_brightness,
343 "Light theme line numbers should be between text and background. Text: {}, Line num: {}, Bg: {}",
344 text_brightness,
345 line_num_brightness,
346 bg_brightness
347 );
348 }
349 }
350 }
351
352 #[test]
353 fn test_color_helper_functions() {
354 let color = Color::from_rgb(0.5, 0.5, 0.5);
355
356 let darker = darken(color, 0.5);
358 assert!(darker.r < color.r);
359 assert!(darker.g < color.g);
360 assert!(darker.b < color.b);
361
362 let lighter = lighten(color, 0.5);
364 assert!(lighter.r > color.r);
365 assert!(lighter.g > color.g);
366 assert!(lighter.b > color.b);
367
368 let dimmed = dim_color(color, 0.5);
370 assert!(dimmed.r < color.r);
371
372 let transparent = with_alpha(color, 0.3);
374 assert!((transparent.a - 0.3).abs() < f32::EPSILON);
375 assert!((transparent.r - color.r).abs() < f32::EPSILON);
376 }
377
378 #[test]
379 fn test_style_copy() {
380 let theme = iced::Theme::Dark;
381 let style1 = from_iced_theme(&theme);
382 let style2 = style1;
383
384 assert!(
386 (style1.background.r - style2.background.r).abs() < f32::EPSILON
387 );
388 assert!(
389 (style1.text_color.r - style2.text_color.r).abs() < f32::EPSILON
390 );
391 assert!(
392 (style1.gutter_background.r - style2.gutter_background.r).abs()
393 < f32::EPSILON
394 );
395 }
396
397 #[test]
398 fn test_catalog_default() {
399 let theme = iced::Theme::Dark;
400 let class = <iced::Theme as Catalog>::default();
401 let style = theme.style(&class);
402
403 assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
405 assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
406 }
407}