use crate::compat::BTreeMap;
use crate::core::{Color, Font};
#[cfg(not(alloc_frugal))]
use serde::{Deserialize, Serialize};
use crate::style::{EasingFunction, MotionCurve, Padding, PaddingSpec, Shadow};
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub appearance: AppearanceMode,
pub colors: Colors,
pub fonts: Fonts,
pub spacing: Spacing,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub icons: IconTokens,
pub borders: Borders,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub flat_surfaces: bool,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub system_accent: SystemAccent,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub metrics_profile: crate::widget::metrics::ControlMetricsProfile,
pub overrides: ThemeOverrides,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub motion: Motion,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(not(alloc_frugal), serde(default))]
pub struct Motion {
pub fast: u32,
pub normal: u32,
pub slow: u32,
pub easing: EasingFunction,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub curve: MotionCurve,
}
impl Default for Motion {
fn default() -> Self {
Self {
fast: 100,
normal: 200,
slow: 300,
easing: EasingFunction::EaseOut,
curve: MotionCurve::Easing,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
pub enum Material {
#[default]
None,
WindowChrome,
Sidebar,
ContentUnder,
}
impl Material {
pub const fn backdrop_blur_radius(self) -> Option<u32> {
match self {
Self::None => None,
Self::WindowChrome => Some(30),
Self::Sidebar => Some(20),
Self::ContentUnder => Some(12),
}
}
pub fn tint(self, colors: &Colors) -> Option<(Color, f32)> {
match self {
Self::None => None,
Self::WindowChrome => Some((colors.surface_container_high, 0.65)),
Self::Sidebar => Some((colors.surface_container, 0.55)),
Self::ContentUnder => Some((colors.background, 0.40)),
}
}
pub fn opaque_surface(self, colors: &Colors) -> Color {
match self {
Self::None | Self::ContentUnder => colors.background,
Self::WindowChrome => colors.surface_container_high,
Self::Sidebar => colors.surface_container,
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AppearanceMode {
#[default]
Light,
Dark,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[cfg_attr(not(alloc_frugal), serde(rename_all = "lowercase"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SystemAccent {
#[default]
Blue,
Purple,
Pink,
Red,
Orange,
Yellow,
Green,
Graphite,
}
impl SystemAccent {
pub const ALL: [SystemAccent; 8] = [
SystemAccent::Blue,
SystemAccent::Purple,
SystemAccent::Pink,
SystemAccent::Red,
SystemAccent::Orange,
SystemAccent::Yellow,
SystemAccent::Green,
SystemAccent::Graphite,
];
pub const fn color(self, dark: bool) -> Color {
match (self, dark) {
(SystemAccent::Blue, false) => Color::rgb(0x00, 0x7A, 0xFF),
(SystemAccent::Blue, true) => Color::rgb(0x0A, 0x84, 0xFF),
(SystemAccent::Purple, false) => Color::rgb(0xAF, 0x52, 0xDE),
(SystemAccent::Purple, true) => Color::rgb(0xBF, 0x5A, 0xF2),
(SystemAccent::Pink, false) => Color::rgb(0xFF, 0x2D, 0x55),
(SystemAccent::Pink, true) => Color::rgb(0xFF, 0x37, 0x5F),
(SystemAccent::Red, false) => Color::rgb(0xFF, 0x3B, 0x30),
(SystemAccent::Red, true) => Color::rgb(0xFF, 0x45, 0x3A),
(SystemAccent::Orange, false) => Color::rgb(0xFF, 0x95, 0x00),
(SystemAccent::Orange, true) => Color::rgb(0xFF, 0x9F, 0x0A),
(SystemAccent::Yellow, false) => Color::rgb(0xFF, 0xCC, 0x00),
(SystemAccent::Yellow, true) => Color::rgb(0xFF, 0xD6, 0x0A),
(SystemAccent::Green, false) => Color::rgb(0x34, 0xC7, 0x59),
(SystemAccent::Green, true) => Color::rgb(0x30, 0xD1, 0x58),
(SystemAccent::Graphite, false) => Color::rgb(0x8E, 0x8E, 0x93),
(SystemAccent::Graphite, true) => Color::rgb(0x98, 0x98, 0x9D),
}
}
pub fn nearest(color: Color) -> Self {
let mut best = SystemAccent::Blue;
let mut best_distance = u32::MAX;
for accent in Self::ALL {
for dark in [false, true] {
let candidate = accent.color(dark);
let dr = i32::from(candidate.r) - i32::from(color.r);
let dg = i32::from(candidate.g) - i32::from(color.g);
let db = i32::from(candidate.b) - i32::from(color.b);
let distance = (dr * dr + dg * dg + db * db) as u32;
if distance < best_distance {
best_distance = distance;
best = accent;
}
}
}
best
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WidgetRole {
#[default]
Surface,
Primary,
Text,
Input,
Accent,
Choice,
Danger,
}
impl WidgetRole {
pub fn for_kind_name(kind_name: &str) -> Self {
let normalized: String = kind_name
.chars()
.filter(|c| !matches!(c, '_' | '-' | ' '))
.flat_map(char::to_lowercase)
.collect();
match normalized.as_str() {
"button" | "toggle" | "togglebutton" => Self::Primary,
"label" | "floatinglabel" | "text" | "heading" | "breadcrumb" | "commandlink"
| "lcdnumber" | "fontpreview" => Self::Text,
"lineedit"
| "textedit"
| "textareainput"
| "textarea"
| "spinbox"
| "doublespinbox"
| "combobox"
| "editablecombobox"
| "fontcombobox"
| "autocompleteedit"
| "maskededit"
| "multiselectcombobox"
| "searchbar"
| "searchbox"
| "taginput"
| "codeeditor"
| "richedit"
| "dateedit"
| "timeedit"
| "timepicker"
| "datetimeedit"
| "inplaceeditor"
| "shortcuteditor"
| "coloreditor" => Self::Input,
"slider" | "rangeslider" | "progressbar" | "progresscircle" | "activityindicator"
| "meter" | "lcdprogress" | "sparkline" => Self::Accent,
"checkbox" | "radiobutton" | "switch" | "checklistbox" | "rating" | "stepper" => {
Self::Choice
}
"listbox" | "listview" | "tableview" | "treeview" | "scrollarea" | "textbrowser"
| "plaintextedit" => Self::Input,
"scrollbar" | "scroll_bar" => Self::Input,
"errordialog" | "trash" | "deletebutton" => Self::Danger,
_ => Self::Surface,
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Colors {
pub background: Color,
pub foreground: Color,
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub error: Color,
pub warning: Color,
pub success: Color,
pub disabled: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_info_color"))]
pub info: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_outline_color"))]
pub outline: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_outline_variant_color"))]
pub outline_variant: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_scrim_color"))]
pub scrim: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_surface_container_color"))]
pub surface_container: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_surface_container_high_color"))]
pub surface_container_high: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_inverse_surface_color"))]
pub inverse_surface: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_on_inverse_surface_color"))]
pub on_inverse_surface: Color,
}
#[cfg(not(alloc_frugal))]
const fn default_info_color() -> Color {
Color::INFO
}
#[cfg(not(alloc_frugal))]
const fn default_outline_color() -> Color {
Color::rgb(121, 116, 126)
}
#[cfg(not(alloc_frugal))]
const fn default_outline_variant_color() -> Color {
Color::rgb(202, 196, 208)
}
#[cfg(not(alloc_frugal))]
const fn default_scrim_color() -> Color {
Color::rgba(0, 0, 0, 82)
}
#[cfg(not(alloc_frugal))]
const fn default_surface_container_color() -> Color {
Color::rgb(243, 237, 247)
}
#[cfg(not(alloc_frugal))]
const fn default_surface_container_high_color() -> Color {
Color::rgb(236, 230, 240)
}
#[cfg(not(alloc_frugal))]
const fn default_inverse_surface_color() -> Color {
Color::rgb(49, 48, 51)
}
#[cfg(not(alloc_frugal))]
const fn default_on_inverse_surface_color() -> Color {
Color::rgb(244, 239, 244)
}
impl Colors {
pub fn blend(&self, other: &Self, weight: f32) -> Self {
Self {
background: self.background.blend(&other.background, weight),
foreground: self.foreground.blend(&other.foreground, weight),
primary: self.primary.blend(&other.primary, weight),
secondary: self.secondary.blend(&other.secondary, weight),
accent: self.accent.blend(&other.accent, weight),
error: self.error.blend(&other.error, weight),
warning: self.warning.blend(&other.warning, weight),
success: self.success.blend(&other.success, weight),
disabled: self.disabled.blend(&other.disabled, weight),
info: self.info.blend(&other.info, weight),
outline: self.outline.blend(&other.outline, weight),
outline_variant: self.outline_variant.blend(&other.outline_variant, weight),
scrim: self.scrim.blend(&other.scrim, weight),
surface_container: self.surface_container.blend(&other.surface_container, weight),
surface_container_high: self
.surface_container_high
.blend(&other.surface_container_high, weight),
inverse_surface: self.inverse_surface.blend(&other.inverse_surface, weight),
on_inverse_surface: self.on_inverse_surface.blend(&other.on_inverse_surface, weight),
}
}
}
impl Default for Colors {
fn default() -> Self {
Self {
background: Color::rgb(240, 240, 240),
foreground: Color::rgb(0, 0, 0),
primary: Color::rgb(33, 150, 243),
secondary: Color::rgb(158, 158, 158),
accent: Color::rgb(255, 152, 0),
error: Color::rgb(244, 67, 54),
warning: Color::rgb(255, 193, 7),
success: Color::rgb(76, 175, 80),
disabled: Color::rgb(200, 200, 200),
info: Color::INFO,
outline: Color::rgb(121, 116, 126),
outline_variant: Color::rgb(202, 196, 208),
scrim: Color::rgba(0, 0, 0, 82),
surface_container: Color::rgb(243, 237, 247),
surface_container_high: Color::rgb(236, 230, 240),
inverse_surface: Color::rgb(49, 48, 51),
on_inverse_surface: Color::rgb(244, 239, 244),
}
}
}
impl Color {
pub fn from_hex(hex: &str) -> Result<Self, String> {
Self::parse_hex(hex).ok_or_else(|| format!("Invalid hex color string: '{hex}'"))
}
pub fn to_hex(&self) -> String {
self.to_hex_rgba()
}
pub fn dark_variant(&self, factor: f32) -> Self {
let f = Self::normalized_factor(factor);
Self::rgba(
(self.r as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
(self.g as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
(self.b as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
self.a,
)
}
pub fn light_variant(&self, factor: f32) -> Self {
let f = Self::normalized_factor(factor);
Self::rgba(
(self.r as f32 + (255.0 - self.r as f32) * f).round().clamp(0.0, 255.0) as u8,
(self.g as f32 + (255.0 - self.g as f32) * f).round().clamp(0.0, 255.0) as u8,
(self.b as f32 + (255.0 - self.b as f32) * f).round().clamp(0.0, 255.0) as u8,
self.a,
)
}
pub fn normalized_factor(factor: f32) -> f32 {
if factor.is_nan() {
0.0
} else {
factor.clamp(0.0, 1.0)
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Fonts {
pub regular: Font,
pub bold: Font,
pub italic: Font,
pub monospace: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_caption_font"))]
pub caption: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_body_font"))]
pub body: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_title_font"))]
pub title: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_headline_font"))]
pub headline: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_display_font"))]
pub display: Font,
}
#[cfg(not(alloc_frugal))]
fn default_caption_font() -> Font {
Font::simple("Arial", 11.0)
}
#[cfg(not(alloc_frugal))]
fn default_body_font() -> Font {
Font::simple("Arial", 14.0)
}
#[cfg(not(alloc_frugal))]
fn default_title_font() -> Font {
Font::bold("Arial", 16.0)
}
#[cfg(not(alloc_frugal))]
fn default_headline_font() -> Font {
Font::bold("Arial", 20.0)
}
#[cfg(not(alloc_frugal))]
fn default_display_font() -> Font {
Font::bold("Arial", 28.0)
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Spacing {
pub small: u32,
pub medium: u32,
pub large: u32,
pub extra_large: u32,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(alloc_frugal), serde(default))]
pub struct IconTokens {
pub small: u32,
pub medium: u32,
pub large: u32,
}
impl Default for IconTokens {
fn default() -> Self {
Self { small: 16, medium: 18, large: 32 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IconSlot {
Small,
Medium,
Large,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Borders {
pub width: u32,
pub radius: u32,
pub shadow: bool,
}
impl Theme {
pub fn icon_size(&self, slot: IconSlot) -> u32 {
match slot {
IconSlot::Small => self.icons.small,
IconSlot::Medium => self.icons.medium,
IconSlot::Large => self.icons.large,
}
}
pub fn elevation(&self, elevation: crate::render::Elevation, tint: Color) -> Option<Shadow> {
if !self.borders.shadow {
return None;
}
crate::render::default_shadow(elevation).map(|level| Shadow {
x: 0,
y: level.y,
blur: level.blur,
color: tint.with_alpha(level.alpha),
})
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ThemeOverrides {
pub styles: BTreeMap<String, ThemeStyleToken>,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ThemeStyleToken {
pub background: Option<Color>,
pub foreground: Option<Color>,
pub border: Option<Color>,
pub border_width: Option<u32>,
pub radius: Option<u32>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub padding: Option<PaddingToken>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub font: Option<Font>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub opacity: Option<f32>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub shadow: ShadowOverride,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub touch_target: Option<[u32; 2]>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub elevation: Option<SurfaceElevationToken>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub bevel: BevelOverride,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub material: Option<SurfaceMaterialToken>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub hairline: Option<SurfaceHairlineToken>,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(not(alloc_frugal), serde(default))]
pub struct PaddingToken {
pub all: Option<u32>,
pub vertical: Option<u32>,
pub horizontal: Option<u32>,
pub top: Option<u32>,
pub right: Option<u32>,
pub bottom: Option<u32>,
pub left: Option<u32>,
}
impl PaddingToken {
pub fn resolve(&self) -> Padding {
let mut spec = PaddingSpec::new();
if let Some(all) = self.all {
spec = spec.set_uniform(all);
}
if let Some(vertical) = self.vertical {
spec = spec.set_vertical(vertical);
}
if let Some(horizontal) = self.horizontal {
spec = spec.set_horizontal(horizontal);
}
if let Some(top) = self.top {
spec = spec.top(top);
}
if let Some(right) = self.right {
spec = spec.right(right);
}
if let Some(bottom) = self.bottom {
spec = spec.bottom(bottom);
}
if let Some(left) = self.left {
spec = spec.left(left);
}
spec.resolve()
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(alloc_frugal), serde(try_from = "String", into = "String"))]
pub struct SurfaceElevationToken(pub crate::render::surface::Elevation);
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(alloc_frugal), serde(try_from = "String", into = "String"))]
pub struct SurfaceMaterialToken(pub crate::render::surface::Material);
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(alloc_frugal), serde(try_from = "String", into = "String"))]
pub struct SurfaceHairlineToken(pub crate::render::surface::Hairline);
#[cfg(not(alloc_frugal))]
pub(crate) fn unknown_surface_token(key: &str, value: &str) -> String {
let accepted = match key {
"elevation" => "flat, level1, level2, level3, level4",
"material" => "solid, translucent",
"hairline" => "outline, shadow, none",
"bevel" => "raised, inset, none, inherit",
_ => "see the surface-style documentation",
};
format!("unknown {key} token {value:?} (accepted: {accepted})")
}
#[cfg(not(alloc_frugal))]
impl TryFrom<String> for SurfaceElevationToken {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
crate::render::surface::Elevation::parse(&value)
.map(Self)
.ok_or_else(|| unknown_surface_token("elevation", &value))
}
}
#[cfg(not(alloc_frugal))]
impl TryFrom<String> for SurfaceMaterialToken {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
crate::render::surface::Material::parse(&value)
.map(Self)
.ok_or_else(|| unknown_surface_token("material", &value))
}
}
#[cfg(not(alloc_frugal))]
impl TryFrom<String> for SurfaceHairlineToken {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
crate::render::surface::Hairline::parse(&value)
.map(Self)
.ok_or_else(|| unknown_surface_token("hairline", &value))
}
}
#[cfg(not(alloc_frugal))]
impl From<SurfaceElevationToken> for String {
fn from(value: SurfaceElevationToken) -> Self {
value.0.as_str().to_string()
}
}
#[cfg(not(alloc_frugal))]
impl From<SurfaceMaterialToken> for String {
fn from(value: SurfaceMaterialToken) -> Self {
value.0.as_str().to_string()
}
}
#[cfg(not(alloc_frugal))]
impl From<SurfaceHairlineToken> for String {
fn from(value: SurfaceHairlineToken) -> Self {
value.0.as_str().to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BevelOverride {
#[default]
Inherit,
None,
Set(crate::render::BevelDirection),
}
#[cfg(not(alloc_frugal))]
impl<'de> serde::Deserialize<'de> for BevelOverride {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
let token = String::deserialize(deserializer)?;
match token.as_str() {
"inherit" => Ok(Self::Inherit),
"none" | "flat" => Ok(Self::None),
other => crate::render::BevelDirection::parse(other)
.map(Self::Set)
.ok_or_else(|| {
D::Error::custom(crate::theme::types::unknown_surface_token("bevel", other))
}),
}
}
}
#[cfg(not(alloc_frugal))]
impl serde::Serialize for BevelOverride {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let token = match self {
Self::Inherit => "inherit",
Self::None => "none",
Self::Set(direction) => direction.as_str(),
};
serializer.serialize_str(token)
}
}
impl BevelOverride {
pub fn apply(
self,
current: Option<crate::render::BevelSpec>,
) -> Option<crate::render::BevelSpec> {
match self {
Self::Inherit => current,
Self::None => None,
Self::Set(direction) => Some(crate::render::BevelSpec::new(direction)),
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ShadowOverride {
#[default]
Inherit,
None,
Set(ShadowToken),
}
impl ShadowOverride {
pub fn apply(self, current: Option<Shadow>) -> Option<Shadow> {
match self {
Self::Inherit => current,
Self::None => None,
Self::Set(token) => Some(token.into()),
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowToken {
#[cfg_attr(not(alloc_frugal), serde(default))]
pub x: i32,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub y: i32,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub blur: u32,
pub color: Color,
}
impl From<ShadowToken> for Shadow {
fn from(token: ShadowToken) -> Self {
Self { x: token.x, y: token.y, blur: token.blur, color: token.color }
}
}
#[cfg(test)]
mod system_accent_tests {
use super::*;
#[test]
fn the_eight_system_accents_use_the_hosts_light_and_dark_values() {
let table = [
(SystemAccent::Blue, Color::rgb(0x00, 0x7A, 0xFF), Color::rgb(0x0A, 0x84, 0xFF)),
(SystemAccent::Purple, Color::rgb(0xAF, 0x52, 0xDE), Color::rgb(0xBF, 0x5A, 0xF2)),
(SystemAccent::Pink, Color::rgb(0xFF, 0x2D, 0x55), Color::rgb(0xFF, 0x37, 0x5F)),
(SystemAccent::Red, Color::rgb(0xFF, 0x3B, 0x30), Color::rgb(0xFF, 0x45, 0x3A)),
(SystemAccent::Orange, Color::rgb(0xFF, 0x95, 0x00), Color::rgb(0xFF, 0x9F, 0x0A)),
(SystemAccent::Yellow, Color::rgb(0xFF, 0xCC, 0x00), Color::rgb(0xFF, 0xD6, 0x0A)),
(SystemAccent::Green, Color::rgb(0x34, 0xC7, 0x59), Color::rgb(0x30, 0xD1, 0x58)),
(SystemAccent::Graphite, Color::rgb(0x8E, 0x8E, 0x93), Color::rgb(0x98, 0x98, 0x9D)),
];
assert_eq!(table.len(), SystemAccent::ALL.len(), "the table covers every variant");
for (accent, light, dark) in table {
assert_eq!(accent.color(false), light, "{accent:?} light value");
assert_eq!(accent.color(true), dark, "{accent:?} dark value");
assert_ne!(light, dark, "{accent:?} must not reuse one value for both appearances");
}
}
#[test]
fn a_read_colour_maps_back_to_the_accent_it_came_from() {
for accent in SystemAccent::ALL {
for dark in [false, true] {
assert_eq!(
SystemAccent::nearest(accent.color(dark)),
accent,
"{accent:?} in dark={dark} must round-trip"
);
}
}
assert_eq!(SystemAccent::nearest(Color::rgb(0x00, 0x7A, 0xFE)), SystemAccent::Blue);
}
#[test]
fn a_palette_blend_is_per_token() {
let light = Colors::default();
let dark = crate::theme::Theme::dark().colors;
let at_start = light.blend(&dark, 0.0);
assert_eq!(at_start.background, light.background, "weight 0 is the source palette");
assert_eq!(at_start.accent, light.accent);
let at_end = light.blend(&dark, 1.0);
assert_eq!(at_end.background, dark.background);
assert_eq!(at_end.foreground, dark.foreground);
assert_eq!(at_end.accent, dark.accent);
let middle = light.blend(&dark, 0.5);
assert_ne!(middle.background, light.background);
assert_ne!(middle.background, dark.background);
assert_ne!(middle.foreground, light.foreground);
assert_ne!(middle.foreground, dark.foreground);
}
}