use std::collections::HashMap;
use crate::tokens::{ColorTheme, CommonTheme};
use crate::{Density, ElevationScale, ShapeOverrides, SpacingScale, TypographyOverrides};
pub fn brand_ramp(primary: &str) -> HashMap<i32, String> {
let (h, s, l) = parse_hex_hsl(primary).unwrap_or((210.0, 0.85, 0.45));
let mut ramp = HashMap::new();
for (variant, target_l) in BRAND_RAMP_LIGHTNESS {
let lightness = if variant == 80 { l } else { target_l };
ramp.insert(variant, hsl_to_hex(h, s, lightness.clamp(0.0, 1.0)));
}
ramp
}
const BRAND_RAMP_LIGHTNESS: [(i32, f32); 16] = [
(10, 0.08),
(20, 0.12),
(30, 0.17),
(40, 0.22),
(50, 0.28),
(60, 0.34),
(70, 0.40),
(80, 0.45),
(90, 0.52),
(100, 0.60),
(110, 0.68),
(120, 0.76),
(130, 0.84),
(140, 0.90),
(150, 0.95),
(160, 0.98),
];
fn parse_hex_hsl(hex: &str) -> Option<(f32, f32, f32)> {
let hex = hex.trim().trim_start_matches('#');
let (r, g, b) = match hex.len() {
6 => (
u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0,
u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0,
u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0,
),
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).ok()? as f32 / 15.0;
let g = u8::from_str_radix(&hex[1..2], 16).ok()? as f32 / 15.0;
let b = u8::from_str_radix(&hex[2..3], 16).ok()? as f32 / 15.0;
(r, g, b)
}
_ => return None,
};
Some(rgb_to_hsl(r, g, b))
}
fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) / 2.0;
if (max - min).abs() < f32::EPSILON {
return (0.0, 0.0, l);
}
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if (max - r).abs() < f32::EPSILON {
((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
} else if (max - g).abs() < f32::EPSILON {
((b - r) / d + 2.0) / 6.0
} else {
((r - g) / d + 4.0) / 6.0
};
(h * 360.0, s, l)
}
fn hsl_to_hex(h: f32, s: f32, l: f32) -> String {
let h = h / 360.0;
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
let g = hue_to_rgb(p, q, h);
let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
format!(
"#{:02x}{:02x}{:02x}",
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8
)
}
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
}
pub fn apply_density(common: &mut CommonTheme, density: Density) {
scale_spacing_fields(common, density.spacing_multiplier());
scale_font_size_fields(common, density.font_size_multiplier());
}
pub fn apply_typography(common: &mut CommonTheme, typography: &TypographyOverrides) {
if let Some(family) = &typography.font_family_base {
common.set_font_family_base(family.clone());
}
if let Some(family) = &typography.font_family_monospace {
common.set_font_family_monospace(family.clone());
}
if let Some(family) = &typography.font_family_numeric {
common.set_font_family_numeric(family.clone());
}
if let Some(family) = &typography.font_family_display {
common.set_font_family_display(family.clone());
}
if let Some(scale) = typography.font_size_scale {
scale_font_size_fields(common, scale);
}
if let Some(scale) = typography.line_height_scale {
scale_line_height_fields(common, scale);
}
}
pub fn apply_spacing_scale(common: &mut CommonTheme, scale: SpacingScale) {
scale_spacing_fields(common, scale.multiplier);
}
pub fn apply_elevation_scale(color: &mut ColorTheme, scale: ElevationScale) {
let m = scale.multiplier;
color.set_shadow2(scale_shadow(color.shadow2(), m));
color.set_shadow4(scale_shadow(color.shadow4(), m));
color.set_shadow8(scale_shadow(color.shadow8(), m));
color.set_shadow16(scale_shadow(color.shadow16(), m));
color.set_shadow28(scale_shadow(color.shadow28(), m));
color.set_shadow64(scale_shadow(color.shadow64(), m));
}
pub fn apply_orbital_shape_defaults(common: &mut CommonTheme) {
common.set_border_radius_small("4px".into());
common.set_border_radius_medium("8px".into());
common.set_border_radius_large("12px".into());
common.set_border_radius_x_large("16px".into());
common.set_border_radius_floating("16px".into());
}
pub fn apply_shape_overrides(common: &mut CommonTheme, shape: &ShapeOverrides) {
if let Some(scale) = shape.border_radius_scale {
if let Some(px) = parse_px(common.border_radius_small()) {
common.set_border_radius_small(format!("{}px", (px * scale).round() as i32));
}
if let Some(px) = parse_px(common.border_radius_medium()) {
common.set_border_radius_medium(format!("{}px", (px * scale).round() as i32));
}
if let Some(px) = parse_px(common.border_radius_large()) {
common.set_border_radius_large(format!("{}px", (px * scale).round() as i32));
}
if let Some(px) = parse_px(common.border_radius_x_large()) {
common.set_border_radius_x_large(format!("{}px", (px * scale).round() as i32));
}
if let Some(px) = parse_px(common.border_radius_floating()) {
common.set_border_radius_floating(format!("{}px", (px * scale).round() as i32));
}
}
}
fn scale_shadow(value: &str, multiplier: f32) -> String {
if (multiplier - 1.0).abs() < f32::EPSILON {
return value.to_string();
}
value
.replace("2px", &format!("{}px", (2.0 * multiplier).round() as i32))
.replace("4px", &format!("{}px", (4.0 * multiplier).round() as i32))
.replace("8px", &format!("{}px", (8.0 * multiplier).round() as i32))
.replace("16px", &format!("{}px", (16.0 * multiplier).round() as i32))
.replace("32px", &format!("{}px", (32.0 * multiplier).round() as i32))
.replace("64px", &format!("{}px", (64.0 * multiplier).round() as i32))
}
fn scale_spacing_fields(common: &mut CommonTheme, multiplier: f32) {
macro_rules! scale_field {
($getter:ident, $setter:ident) => {
if let Some(px) = parse_px(common.$getter()) {
common.$setter(format!("{}px", (px * multiplier).round() as i32));
}
};
}
scale_field!(spacing_horizontal_none, set_spacing_horizontal_none);
scale_field!(spacing_horizontal_x_x_s, set_spacing_horizontal_x_x_s);
scale_field!(spacing_horizontal_x_s, set_spacing_horizontal_x_s);
scale_field!(spacing_horizontal_s_nudge, set_spacing_horizontal_s_nudge);
scale_field!(spacing_horizontal_s, set_spacing_horizontal_s);
scale_field!(spacing_horizontal_m_nudge, set_spacing_horizontal_m_nudge);
scale_field!(spacing_horizontal_m, set_spacing_horizontal_m);
scale_field!(spacing_horizontal_l, set_spacing_horizontal_l);
scale_field!(spacing_horizontal_x_l, set_spacing_horizontal_x_l);
scale_field!(spacing_horizontal_x_x_l, set_spacing_horizontal_x_x_l);
scale_field!(spacing_horizontal_x_x_x_l, set_spacing_horizontal_x_x_x_l);
scale_field!(spacing_vertical_none, set_spacing_vertical_none);
scale_field!(spacing_vertical_x_x_s, set_spacing_vertical_x_x_s);
scale_field!(spacing_vertical_x_s, set_spacing_vertical_x_s);
scale_field!(spacing_vertical_s_nudge, set_spacing_vertical_s_nudge);
scale_field!(spacing_vertical_s, set_spacing_vertical_s);
scale_field!(spacing_vertical_m_nudge, set_spacing_vertical_m_nudge);
scale_field!(spacing_vertical_m, set_spacing_vertical_m);
scale_field!(spacing_vertical_l, set_spacing_vertical_l);
scale_field!(spacing_vertical_x_l, set_spacing_vertical_x_l);
scale_field!(spacing_vertical_x_x_l, set_spacing_vertical_x_x_l);
scale_field!(spacing_vertical_x_x_x_l, set_spacing_vertical_x_x_x_l);
}
fn scale_font_size_fields(common: &mut CommonTheme, multiplier: f32) {
macro_rules! scale_field {
($getter:ident, $setter:ident) => {
if let Some(px) = parse_px(common.$getter()) {
common.$setter(format!("{}px", (px * multiplier).round() as i32));
}
};
}
scale_field!(font_size_base_100, set_font_size_base_100);
scale_field!(font_size_base_200, set_font_size_base_200);
scale_field!(font_size_base_300, set_font_size_base_300);
scale_field!(font_size_base_400, set_font_size_base_400);
scale_field!(font_size_base_500, set_font_size_base_500);
scale_field!(font_size_base_600, set_font_size_base_600);
scale_field!(font_size_base_700, set_font_size_base_700);
scale_field!(font_size_base_800, set_font_size_base_800);
scale_field!(font_size_base_900, set_font_size_base_900);
scale_field!(font_size_base_1000, set_font_size_base_1000);
}
fn scale_line_height_fields(common: &mut CommonTheme, multiplier: f32) {
macro_rules! scale_field {
($getter:ident, $setter:ident) => {
if let Some(px) = parse_px(common.$getter()) {
common.$setter(format!("{}px", (px * multiplier).round() as i32));
}
};
}
scale_field!(line_height_base_200, set_line_height_base_200);
scale_field!(line_height_base_300, set_line_height_base_300);
scale_field!(line_height_base_400, set_line_height_base_400);
scale_field!(line_height_base_500, set_line_height_base_500);
}
fn parse_px(value: &str) -> Option<f32> {
value.trim().strip_suffix("px")?.parse().ok()
}
#[cfg(test)]
mod tests {
use crate::Theme;
#[test]
fn orbital_shape_defaults_apply_on_theme_for_mode() {
let theme = Theme::light();
assert_eq!(theme.common.border_radius_small(), "4px");
assert_eq!(theme.common.border_radius_medium(), "8px");
assert_eq!(theme.common.border_radius_large(), "12px");
assert_eq!(theme.common.border_radius_x_large(), "16px");
assert_eq!(theme.common.border_radius_floating(), "16px");
}
}