use iced::{
Border, Color, Shadow,
widget::button,
};
use snora_design::Tokens;
use super::color::to_iced_color;
fn lighten(color: Color, amount: f32) -> Color {
Color {
r: (color.r + amount).min(1.0),
g: (color.g + amount).min(1.0),
b: (color.b + amount).min(1.0),
a: color.a,
}
}
fn darken(color: Color, amount: f32) -> Color {
Color {
r: (color.r - amount).max(0.0),
g: (color.g - amount).max(0.0),
b: (color.b - amount).max(0.0),
a: color.a,
}
}
fn disabled_alpha(color: Color) -> Color {
Color { a: color.a * 0.45, ..color }
}
fn border_radius(tokens: &Tokens) -> iced::border::Radius {
tokens.radius.md.into()
}
#[must_use]
pub fn primary(tokens: &Tokens, status: button::Status) -> button::Style {
let accent = to_iced_color(tokens.palette.accent);
let text = to_iced_color(tokens.palette.accent_text);
let radius = border_radius(tokens);
let (bg, text_color) = match status {
button::Status::Active => (accent, text),
button::Status::Hovered => (lighten(accent, 0.06), text),
button::Status::Pressed => (darken(accent, 0.06), text),
button::Status::Disabled => (disabled_alpha(accent), disabled_alpha(text)),
};
button::Style {
background: Some(bg.into()),
text_color,
border: Border::default().rounded(radius),
shadow: Shadow::default(),
snap: true,
}
}
#[must_use]
pub fn secondary(tokens: &Tokens, status: button::Status) -> button::Style {
let accent = to_iced_color(tokens.palette.accent);
let radius = border_radius(tokens);
let (bg, text_color, border_color) = match status {
button::Status::Active => (Color::TRANSPARENT, accent, accent),
button::Status::Hovered => (
Color { a: 0.08, ..accent },
lighten(accent, 0.06),
lighten(accent, 0.06),
),
button::Status::Pressed => (
Color { a: 0.14, ..accent },
darken(accent, 0.06),
darken(accent, 0.06),
),
button::Status::Disabled => (
Color::TRANSPARENT,
disabled_alpha(accent),
disabled_alpha(accent),
),
};
button::Style {
background: Some(bg.into()),
text_color,
border: Border::default().rounded(radius).color(border_color).width(1.0),
shadow: Shadow::default(),
snap: true,
}
}
#[must_use]
pub fn ghost(tokens: &Tokens, status: button::Status) -> button::Style {
let accent = to_iced_color(tokens.palette.accent);
let radius = border_radius(tokens);
let (bg, text_color) = match status {
button::Status::Active => (Color::TRANSPARENT, accent),
button::Status::Hovered => (Color { a: 0.08, ..accent }, accent),
button::Status::Pressed => (Color { a: 0.14, ..accent }, darken(accent, 0.06)),
button::Status::Disabled => (Color::TRANSPARENT, disabled_alpha(accent)),
};
button::Style {
background: Some(bg.into()),
text_color,
border: Border::default().rounded(radius),
shadow: Shadow::default(),
snap: true,
}
}
#[must_use]
pub fn danger(tokens: &Tokens, status: button::Status) -> button::Style {
let bg_color = to_iced_color(tokens.palette.danger);
let text = to_iced_color(tokens.palette.danger_text);
let radius = border_radius(tokens);
let (bg, text_color) = match status {
button::Status::Active => (bg_color, text),
button::Status::Hovered => (lighten(bg_color, 0.06), text),
button::Status::Pressed => (darken(bg_color, 0.06), text),
button::Status::Disabled => (disabled_alpha(bg_color), disabled_alpha(text)),
};
button::Style {
background: Some(bg.into()),
text_color,
border: Border::default().rounded(radius),
shadow: Shadow::default(),
snap: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use snora_design::Tokens;
fn all_statuses() -> [button::Status; 4] {
[
button::Status::Active,
button::Status::Hovered,
button::Status::Pressed,
button::Status::Disabled,
]
}
#[test]
fn primary_returns_valid_style_for_all_statuses() {
let t = Tokens::light();
for s in all_statuses() {
let style = primary(&t, s);
assert!(style.background.is_some(), "primary {s:?}: background must be set");
}
}
#[test]
fn secondary_active_has_transparent_background() {
let t = Tokens::dark();
let style = secondary(&t, button::Status::Active);
assert!(
matches!(style.background, Some(iced::Background::Color(c)) if c.a < 0.01),
"secondary active should be transparent, got {:?}", style.background
);
}
#[test]
fn ghost_active_has_transparent_background() {
let t = Tokens::high_contrast_light();
let style = ghost(&t, button::Status::Active);
assert!(
matches!(style.background, Some(iced::Background::Color(c)) if c.a < 0.01),
"ghost active should be transparent"
);
}
#[test]
fn danger_all_statuses_compile_and_produce_background() {
let t = Tokens::high_contrast_dark();
for s in all_statuses() {
let style = danger(&t, s);
assert!(style.background.is_some(), "danger {s:?}: background must be set");
}
}
#[test]
fn disabled_reduces_alpha() {
let t = Tokens::light();
let active = primary(&t, button::Status::Active);
let disabled = primary(&t, button::Status::Disabled);
assert!(
disabled.text_color.a < active.text_color.a,
"disabled alpha {:.2} should be < active alpha {:.2}",
disabled.text_color.a, active.text_color.a
);
}
#[test]
fn all_variants_use_all_presets() {
for tokens in [
Tokens::light(),
Tokens::dark(),
Tokens::high_contrast_light(),
Tokens::high_contrast_dark(),
] {
for status in all_statuses() {
let _ = primary(&tokens, status);
let _ = secondary(&tokens, status);
let _ = ghost(&tokens, status);
let _ = danger(&tokens, status);
}
}
}
}