use super::stored_alpha;
use crate::value::{Number, Value};
pub(crate) fn suggestions(name: &str, pos_args: &[Value], named: &[(String, Value)]) -> Option<Vec<String>> {
if let Some(space) = channel_space(name) {
return Some(vec![format!(
"color.channel($color, \"{name}\", $space: {space})"
)]);
}
let (channel, sign, param) = adjuster(name)?;
let color = arg(pos_args, named, 0, "color")?;
let amount = arg(pos_args, named, 1, param)?;
let Value::Color(color) = color else { return None };
let Value::Number(amount) = amount else {
return None;
};
if channel == "hue" {
let deg = match amount.unit() {
"rad" => amount.value.to_degrees(),
"grad" => amount.value * 360.0 / 400.0,
"turn" => amount.value * 360.0,
_ => amount.value,
};
return Some(vec![adjust_line(channel, deg, "deg")]);
}
let (current, limit, unit) = match channel {
"alpha" => (stored_alpha(color), 1.0, ""),
"saturation" => (hsl_channel(color, 1), 100.0, "%"),
_ => (hsl_channel(color, 2), 100.0, "%"),
};
let signed = sign * amount.value;
let mut out = Vec::with_capacity(2);
if signed != 0.0 {
let room = if signed > 0.0 { limit - current } else { current };
let pct = (signed / room * 100.0).clamp(-100.0, 100.0);
out.push(format!("color.scale($color, ${channel}: {})", number(pct, "%")));
}
out.push(adjust_line(channel, signed, unit));
Some(out)
}
fn hsl_channel(color: &crate::value::Color, i: usize) -> f64 {
if let Some(m) = &color.modern {
if m.space == crate::value::ColorSpace::Hsl {
return m.channels[i].unwrap_or(0.0);
}
}
let hsl = color.to_hsl();
let channel = if i == 1 { hsl.1 } else { hsl.2 };
channel * 100.0
}
fn adjust_line(channel: &str, amount: f64, unit: &str) -> String {
format!("color.adjust($color, ${channel}: {})", number(amount, unit))
}
fn number(value: f64, unit: &str) -> String {
Value::Number(Number::with_unit(value, unit)).to_css(false)
}
fn channel_space(name: &str) -> Option<&'static str> {
Some(match name {
"red" | "green" | "blue" => "rgb",
"hue" | "saturation" | "lightness" => "hsl",
"whiteness" | "blackness" => "hwb",
_ => return None,
})
}
fn adjuster(name: &str) -> Option<(&'static str, f64, &'static str)> {
Some(match name {
"lighten" => ("lightness", 1.0, "amount"),
"darken" => ("lightness", -1.0, "amount"),
"saturate" => ("saturation", 1.0, "amount"),
"desaturate" => ("saturation", -1.0, "amount"),
"opacify" | "fade-in" => ("alpha", 1.0, "amount"),
"transparentize" | "fade-out" => ("alpha", -1.0, "amount"),
"adjust-hue" => ("hue", 1.0, "degrees"),
_ => return None,
})
}
fn arg<'a>(pos_args: &'a [Value], named: &'a [(String, Value)], i: usize, name: &str) -> Option<&'a Value> {
pos_args
.get(i)
.or_else(|| named.iter().find(|(n, _)| n == name).map(|(_, v)| v))
}
pub(crate) fn deprecates(name: &str) -> bool {
channel_space(name).is_some() || adjuster(name).is_some()
}
#[cfg(test)]
mod tests {
use super::{deprecates, suggestions};
use crate::value::{Color, Number, Value};
fn every_builtin_name() -> Vec<&'static str> {
let mut v: Vec<&'static str> = crate::builtins::color::NAMES
.iter()
.chain(crate::builtins::color::MODERN_NAMES.iter())
.chain(crate::builtins::color_ext::NAMES.iter())
.chain(crate::builtins::string::NAMES.iter())
.chain(crate::builtins::map::NAMES.iter())
.chain(crate::builtins::list::NAMES.iter())
.chain(crate::builtins::meta::NAMES.iter())
.chain(crate::builtins::selector::NAMES.iter())
.chain(crate::builtins::math::NAMES.iter())
.copied()
.collect();
v.push("whiteness");
v.push("blackness");
v.sort_unstable();
v.dedup();
v
}
#[test]
fn the_name_gate_agrees_with_the_suggestion_tables() {
let args = [
Value::Color(Color::rgb(171.0, 205.0, 239.0, 1.0)),
Value::Number(Number::with_unit(10.0, "%")),
];
let mut gated = Vec::new();
for name in every_builtin_name() {
let gate = deprecates(name);
let sug = suggestions(name, &args, &[]).is_some();
assert_eq!(gate, sug, "`{name}`: gate says {gate}, suggestions() says {sug}");
if sug {
gated.push(name);
}
}
assert_eq!(
gated,
[
"adjust-hue",
"blackness",
"blue",
"darken",
"desaturate",
"fade-in",
"fade-out",
"green",
"hue",
"lighten",
"lightness",
"opacify",
"red",
"saturate",
"saturation",
"transparentize",
"whiteness",
]
);
}
#[test]
fn the_gate_over_admits_and_never_under_admits() {
assert!(deprecates("lighten"));
assert!(suggestions("lighten", &[], &[]).is_none());
let color = [Value::Color(Color::rgb(1.0, 2.0, 3.0, 1.0))];
assert!(suggestions("lighten", &color, &[]).is_none());
assert!(deprecates("red"));
assert!(suggestions("red", &[], &[]).is_some());
}
}