use iced::Color;
#[derive(Debug, Clone, Copy)]
pub struct Style {
pub background: Color,
pub text_color: Color,
pub gutter_background: Color,
pub gutter_border: Color,
pub line_number_color: Color,
pub scrollbar_background: Color,
pub scroller_color: Color,
pub current_line_highlight: Color,
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>) -> Style;
}
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for iced::Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(from_iced_theme)
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
pub fn from_iced_theme(theme: &iced::Theme) -> Style {
let palette = theme.extended_palette();
let is_dark = palette.is_dark;
let background = palette.background.base.color;
let text_color = palette.background.base.text;
let gutter_background = palette.background.weak.color;
let gutter_border = if is_dark {
darken(palette.background.strong.color, 0.1)
} else {
lighten(palette.background.strong.color, 0.1)
};
let line_number_color = if is_dark {
dim_color(text_color, 0.5)
} else {
blend_colors(text_color, background, 0.5)
};
let scrollbar_background = background;
let scroller_color = palette.secondary.weak.color;
let current_line_highlight = with_alpha(
palette.primary.weak.color,
if is_dark { 0.15 } else { 0.25 },
);
Style {
background,
text_color,
gutter_background,
gutter_border,
line_number_color,
scrollbar_background,
scroller_color,
current_line_highlight,
}
}
fn darken(color: Color, factor: f32) -> Color {
Color {
r: color.r * (1.0 - factor),
g: color.g * (1.0 - factor),
b: color.b * (1.0 - factor),
a: color.a,
}
}
fn lighten(color: Color, factor: f32) -> Color {
Color {
r: color.r + (1.0 - color.r) * factor,
g: color.g + (1.0 - color.g) * factor,
b: color.b + (1.0 - color.b) * factor,
a: color.a,
}
}
fn dim_color(color: Color, factor: f32) -> Color {
Color {
r: color.r * factor,
g: color.g * factor,
b: color.b * factor,
a: color.a,
}
}
fn blend_colors(color1: Color, color2: Color, factor: f32) -> Color {
Color {
r: color1.r + (color2.r - color1.r) * factor,
g: color1.g + (color2.g - color1.g) * factor,
b: color1.b + (color2.b - color1.b) * factor,
a: color1.a + (color2.a - color1.a) * factor,
}
}
fn with_alpha(color: Color, alpha: f32) -> Color {
Color { r: color.r, g: color.g, b: color.b, a: alpha }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_iced_theme_dark() {
let theme = iced::Theme::Dark;
let style = from_iced_theme(&theme);
let brightness =
(style.background.r + style.background.g + style.background.b)
/ 3.0;
assert!(brightness < 0.5, "Dark theme should have dark background");
let text_brightness =
(style.text_color.r + style.text_color.g + style.text_color.b)
/ 3.0;
assert!(text_brightness > 0.5, "Dark theme should have bright text");
}
#[test]
fn test_from_iced_theme_light() {
let theme = iced::Theme::Light;
let style = from_iced_theme(&theme);
let brightness =
(style.background.r + style.background.g + style.background.b)
/ 3.0;
assert!(brightness > 0.5, "Light theme should have bright background");
let text_brightness =
(style.text_color.r + style.text_color.g + style.text_color.b)
/ 3.0;
assert!(text_brightness < 0.5, "Light theme should have dark text");
}
#[test]
fn test_all_iced_themes_produce_valid_styles() {
for theme in iced::Theme::ALL {
let style = from_iced_theme(theme);
assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
assert!(
style.gutter_background.r >= 0.0
&& style.gutter_background.r <= 1.0
);
assert!(
style.line_number_color.r >= 0.0
&& style.line_number_color.r <= 1.0
);
assert!(
style.current_line_highlight.a < 1.0,
"Current line highlight should be semi-transparent for theme: {:?}",
theme
);
}
}
#[test]
fn test_tokyo_night_themes() {
let tokyo_night = iced::Theme::TokyoNight;
let style = from_iced_theme(&tokyo_night);
assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
let tokyo_storm = iced::Theme::TokyoNightStorm;
let style = from_iced_theme(&tokyo_storm);
assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
let tokyo_light = iced::Theme::TokyoNightLight;
let style = from_iced_theme(&tokyo_light);
let brightness =
(style.background.r + style.background.g + style.background.b)
/ 3.0;
assert!(
brightness > 0.5,
"Tokyo Night Light should have bright background"
);
}
#[test]
fn test_catppuccin_themes() {
let themes = [
iced::Theme::CatppuccinLatte,
iced::Theme::CatppuccinFrappe,
iced::Theme::CatppuccinMacchiato,
iced::Theme::CatppuccinMocha,
];
for theme in themes {
let style = from_iced_theme(&theme);
assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
}
}
#[test]
fn test_gutter_colors_distinct_from_background() {
let theme = iced::Theme::Dark;
let style = from_iced_theme(&theme);
let gutter_diff = (style.gutter_background.r - style.background.r)
.abs()
+ (style.gutter_background.g - style.background.g).abs()
+ (style.gutter_background.b - style.background.b).abs();
assert!(
gutter_diff > 0.0,
"Gutter should be visually distinct from background"
);
}
#[test]
fn test_line_numbers_visible_but_subtle() {
for theme in [iced::Theme::Dark, iced::Theme::Light] {
let style = from_iced_theme(&theme);
let palette = theme.extended_palette();
let line_num_brightness = (style.line_number_color.r
+ style.line_number_color.g
+ style.line_number_color.b)
/ 3.0;
let text_brightness =
(style.text_color.r + style.text_color.g + style.text_color.b)
/ 3.0;
let bg_brightness =
(style.background.r + style.background.g + style.background.b)
/ 3.0;
if palette.is_dark {
assert!(
line_num_brightness < text_brightness,
"Dark theme line numbers should be dimmer than text. Line num: {}, Text: {}",
line_num_brightness,
text_brightness
);
} else {
assert!(
line_num_brightness > text_brightness
&& line_num_brightness < bg_brightness,
"Light theme line numbers should be between text and background. Text: {}, Line num: {}, Bg: {}",
text_brightness,
line_num_brightness,
bg_brightness
);
}
}
}
#[test]
fn test_color_helper_functions() {
let color = Color::from_rgb(0.5, 0.5, 0.5);
let darker = darken(color, 0.5);
assert!(darker.r < color.r);
assert!(darker.g < color.g);
assert!(darker.b < color.b);
let lighter = lighten(color, 0.5);
assert!(lighter.r > color.r);
assert!(lighter.g > color.g);
assert!(lighter.b > color.b);
let dimmed = dim_color(color, 0.5);
assert!(dimmed.r < color.r);
let transparent = with_alpha(color, 0.3);
assert!((transparent.a - 0.3).abs() < f32::EPSILON);
assert!((transparent.r - color.r).abs() < f32::EPSILON);
}
#[test]
fn test_style_copy() {
let theme = iced::Theme::Dark;
let style1 = from_iced_theme(&theme);
let style2 = style1;
assert!(
(style1.background.r - style2.background.r).abs() < f32::EPSILON
);
assert!(
(style1.text_color.r - style2.text_color.r).abs() < f32::EPSILON
);
assert!(
(style1.gutter_background.r - style2.gutter_background.r).abs()
< f32::EPSILON
);
}
#[test]
fn test_catalog_default() {
let theme = iced::Theme::Dark;
let class = <iced::Theme as Catalog>::default();
let style = theme.style(&class);
assert!(style.background.r >= 0.0 && style.background.r <= 1.0);
assert!(style.text_color.r >= 0.0 && style.text_color.r <= 1.0);
}
}