use super::{Status, StyleFn};
use iced_core::{Background, Color, Theme};
use iced_widget::{container, text, text_input};
#[derive(Clone, Copy, Debug)]
pub struct Style {
pub button_background: Option<Background>,
pub icon_color: Color,
}
impl Default for Style {
fn default() -> Self {
Self {
button_background: None,
icon_color: Color::BLACK,
}
}
}
pub trait Catalog {
type Class<'a>;
fn default<'a>() -> Self::Class<'a>;
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self, Style>;
fn default<'a>() -> Self::Class<'a> {
Box::new(primary)
}
fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
pub trait ExtendedCatalog:
text_input::Catalog + container::Catalog + text::Catalog + self::Catalog
{
#[must_use]
fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
<Self as text_input::Catalog>::default()
}
fn style(&self, class: &<Self as self::Catalog>::Class<'_>, status: Status) -> Style;
}
impl ExtendedCatalog for Theme {
fn style(&self, class: &<Self as self::Catalog>::Class<'_>, status: Status) -> Style {
class(self, status)
}
}
#[must_use]
pub fn primary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let base = Style {
button_background: Some(palette.primary.strong.color.into()),
icon_color: palette.primary.strong.text,
};
match status {
Status::Disabled => Style {
button_background: base.button_background.map(|bg| match bg {
Background::Color(color) => Background::Color(Color {
a: color.a * 0.5,
..color
}),
Background::Gradient(grad) => Background::Gradient(grad),
}),
icon_color: Color {
a: base.icon_color.a * 0.5,
..base.icon_color
},
},
_ => base,
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced_core::Theme;
#[test]
fn style_default() {
let style = Style::default();
assert!(style.button_background.is_none());
assert_eq!(style.icon_color, Color::BLACK);
}
#[test]
fn primary_theme_active() {
let theme = Theme::TokyoNight;
let style = primary(&theme, Status::Active);
assert!(style.button_background.is_some());
#[allow(clippy::panic)]
if let Some(Background::Color(_)) = style.button_background {
} else {
panic!("Expected button_background to be Some(Background::Color)");
}
}
#[test]
fn primary_theme_hovered() {
let theme = Theme::TokyoNight;
let style = primary(&theme, Status::Hovered);
assert!(style.button_background.is_some());
}
#[test]
fn primary_theme_focused() {
let theme = Theme::TokyoNight;
let style = primary(&theme, Status::Focused);
assert!(style.button_background.is_some());
}
#[test]
fn primary_theme_selected() {
let theme = Theme::TokyoNight;
let style = primary(&theme, Status::Selected);
assert!(style.button_background.is_some());
}
#[test]
fn primary_theme_disabled() {
let theme = Theme::TokyoNight;
let style = primary(&theme, Status::Disabled);
assert!(style.button_background.is_some());
}
#[test]
fn disabled_reduces_alpha() {
let theme = Theme::TokyoNight;
let base_style = primary(&theme, Status::Active);
let disabled_style = primary(&theme, Status::Disabled);
assert!(
disabled_style.icon_color.a <= base_style.icon_color.a,
"Disabled icon color should have reduced alpha"
);
if let (Some(Background::Color(base_color)), Some(Background::Color(disabled_color))) = (
base_style.button_background,
disabled_style.button_background,
) {
assert!(
disabled_color.a <= base_color.a,
"Disabled button background should have reduced alpha"
);
}
}
#[test]
fn disabled_has_half_alpha() {
let theme = Theme::TokyoNight;
let base_style = primary(&theme, Status::Active);
let disabled_style = primary(&theme, Status::Disabled);
let expected_icon_alpha = base_style.icon_color.a * 0.5;
assert!(
(disabled_style.icon_color.a - expected_icon_alpha).abs() < 0.01,
"Disabled icon alpha should be approximately half"
);
}
#[test]
fn non_disabled_statuses_use_base_style() {
let theme = Theme::TokyoNight;
let active_style = primary(&theme, Status::Active);
let hovered_style = primary(&theme, Status::Hovered);
let focused_style = primary(&theme, Status::Focused);
assert_eq!(
format!("{:?}", active_style.button_background),
format!("{:?}", hovered_style.button_background)
);
assert_eq!(
format!("{:?}", active_style.button_background),
format!("{:?}", focused_style.button_background)
);
assert_eq!(active_style.icon_color, hovered_style.icon_color);
assert_eq!(active_style.icon_color, focused_style.icon_color);
}
#[test]
fn catalog_default_class() {
let _class = <Theme as Catalog>::default();
}
#[test]
fn catalog_style() {
let theme = Theme::TokyoNight;
let class = <Theme as Catalog>::default();
let style = <Theme as Catalog>::style(&theme, &class, Status::Active);
assert!(style.button_background.is_some());
}
#[test]
fn catalog_style_with_different_statuses() {
let theme = Theme::TokyoNight;
let class = <Theme as Catalog>::default();
let active_style = <Theme as Catalog>::style(&theme, &class, Status::Active);
let hovered_style = <Theme as Catalog>::style(&theme, &class, Status::Hovered);
let disabled_style = <Theme as Catalog>::style(&theme, &class, Status::Disabled);
assert!(active_style.button_background.is_some());
assert!(hovered_style.button_background.is_some());
assert!(disabled_style.button_background.is_some());
}
#[test]
fn extended_catalog_style() {
let theme = Theme::TokyoNight;
let class = <Theme as Catalog>::default();
let style = <Theme as ExtendedCatalog>::style(&theme, &class, Status::Active);
assert!(style.button_background.is_some());
}
#[test]
fn extended_catalog_default_input() {
let _class = <Theme as ExtendedCatalog>::default_input();
}
#[test]
fn disabled_preserves_gradient_backgrounds() {
let gradient = iced_core::Gradient::Linear(iced_core::gradient::Linear {
angle: 0.0.into(),
stops: [None; 8],
});
let style_with_gradient = Style {
button_background: Some(Background::Gradient(gradient)),
icon_color: Color::WHITE,
};
#[allow(clippy::panic)]
if let Some(Background::Color(_)) = style_with_gradient.button_background {
panic!("Expected gradient to be preserved");
}
}
}