use rustc_hash::FxHashMap;
use crate::color::Color;
use crate::env::Appearance;
use crate::metrics::Metrics;
use crate::schema::{METRIC_ROLES, THEME_ROLES};
use crate::theme::Theme;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ColorToken {
Value {
light: Color,
dark: Color,
},
Derived {
from: TokenRef,
ops: OpRange,
},
}
impl ColorToken {
pub fn same(c: Color) -> Self {
Self::Value { light: c, dark: c }
}
pub fn themed(light: Color, dark: Color) -> Self {
Self::Value { light, dark }
}
pub fn halves(&self) -> Option<(Color, Color)> {
match *self {
Self::Value { light, dark } => Some((light, dark)),
Self::Derived { .. } => None,
}
}
pub fn is_derived(&self) -> bool {
matches!(self, Self::Derived { .. })
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ColorOp {
Lift(f32),
Darken(f32),
Raise(f32),
Alpha(f32),
Mix(String, f32),
Readable(String, f32),
}
impl ColorOp {
pub const VERBS: [&'static str; 6] = ["lift", "darken", "raise", "alpha", "mix", "readable"];
pub fn verb(&self) -> &'static str {
match self {
Self::Lift(_) => "lift",
Self::Darken(_) => "darken",
Self::Raise(_) => "raise",
Self::Alpha(_) => "alpha",
Self::Mix(..) => "mix",
Self::Readable(..) => "readable",
}
}
pub fn takes_color(verb: &str) -> Option<bool> {
match verb {
"lift" | "darken" | "raise" | "alpha" => Some(false),
"mix" | "readable" => Some(true),
_ => None,
}
}
pub fn parse(verb: &str, color: Option<&str>, t: f32) -> Option<Self> {
Some(match (verb, color) {
("lift", None) => Self::Lift(t),
("darken", None) => Self::Darken(t),
("raise", None) => Self::Raise(t),
("alpha", None) => Self::Alpha(t),
("mix", Some(c)) => Self::Mix(c.to_string(), t),
("readable", Some(c)) => Self::Readable(c.to_string(), t),
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Step {
Lift(f32),
Darken(f32),
Raise(f32),
Alpha(f32),
Mix(TokenRef, f32),
Readable(TokenRef, f32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OpRange {
start: u16,
len: u16,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unresolved {
pub token: String,
pub source: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TokenKind {
Color,
Length,
}
impl TokenKind {
pub fn name(self) -> &'static str {
match self {
TokenKind::Color => "color",
TokenKind::Length => "length",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TokenRef {
ColorRole(u16),
LengthRole(u16),
Color(u16),
Length(u16),
}
impl TokenRef {
pub fn kind(self) -> TokenKind {
match self {
TokenRef::ColorRole(_) | TokenRef::Color(_) => TokenKind::Color,
TokenRef::LengthRole(_) | TokenRef::Length(_) => TokenKind::Length,
}
}
pub fn index(self) -> u16 {
match self {
TokenRef::ColorRole(i) | TokenRef::LengthRole(i) => i,
TokenRef::Color(i) => COLOR_ROLES + i,
TokenRef::Length(i) => LENGTH_ROLES + i,
}
}
}
pub const COLOR_ROLES: u16 = THEME_ROLES.len() as u16;
pub const LENGTH_ROLES: u16 = METRIC_ROLES.len() as u16;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TokenError {
Unknown(String),
Kind {
name: String,
is: TokenKind,
wanted: TokenKind,
},
}
impl std::fmt::Display for TokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TokenError::Unknown(name) => write!(
f,
"`${name}` names no token: nothing declared it and it is not a theme or metrics \
role, so this declaration paints nothing"
),
TokenError::Kind { name, is, wanted } => write!(
f,
"`${name}` is a {} token, and this slot takes a {}",
is.name(),
wanted.name()
),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Tokens {
colors: Vec<(String, ColorToken)>,
lengths: Vec<(String, f32)>,
by_name: FxHashMap<String, TokenRef>,
ops: Vec<Step>,
reserved: Vec<String>,
unresolved: Vec<Unresolved>,
}
impl Tokens {
pub fn new() -> Self {
Self::default()
}
pub fn color(self, name: impl Into<String>, c: Color) -> Self {
self.color_token(name, ColorToken::same(c))
}
pub fn color_themed(self, name: impl Into<String>, light: Color, dark: Color) -> Self {
self.color_token(name, ColorToken::themed(light, dark))
}
pub fn color_token(mut self, name: impl Into<String>, token: ColorToken) -> Self {
let name = name.into();
if is_role(&name) {
self.reserved.push(name);
return self;
}
match self.by_name.get(&name) {
Some(TokenRef::Color(i)) => self.colors[*i as usize].1 = token,
_ => {
let i = self.colors.len() as u16;
self.colors.push((name.clone(), token));
self.by_name.insert(name, TokenRef::Color(i));
}
}
self
}
pub fn derive(
mut self,
name: impl Into<String>,
from: &str,
ops: impl IntoIterator<Item = ColorOp>,
) -> Self {
let name = name.into();
if is_role(&name) {
self.reserved.push(name);
return self;
}
let own = match self.by_name.get(&name) {
Some(TokenRef::Color(i)) => *i,
_ => self.colors.len() as u16,
};
let source = |t: &Self, s: &str| -> Option<TokenRef> {
match role_ref(s) {
Some(r @ TokenRef::ColorRole(_)) => Some(r),
Some(_) => None,
None => match t.color_id(s) {
Some(r @ TokenRef::Color(i)) if i < own => Some(r),
_ => None,
},
}
};
let unresolved = |s: &str| Unresolved {
token: name.clone(),
source: s.to_string(),
};
let Some(from) = source(&self, from) else {
self.unresolved.push(unresolved(from));
return self;
};
let mut steps = Vec::new();
for op in ops {
let step = match op {
ColorOp::Lift(t) => Step::Lift(t),
ColorOp::Darken(t) => Step::Darken(t),
ColorOp::Raise(t) => Step::Raise(t),
ColorOp::Alpha(a) => Step::Alpha(a),
ColorOp::Mix(ref other, t) => match source(&self, other) {
Some(r) => Step::Mix(r, t),
None => {
self.unresolved.push(unresolved(other));
return self;
}
},
ColorOp::Readable(ref on, ratio) => match source(&self, on) {
Some(r) => Step::Readable(r, ratio),
None => {
self.unresolved.push(unresolved(on));
return self;
}
},
};
steps.push(step);
}
let range = OpRange {
start: self.ops.len() as u16,
len: steps.len() as u16,
};
self.ops.extend(steps);
self.color_token(name, ColorToken::Derived { from, ops: range })
}
pub fn length(mut self, name: impl Into<String>, px: f32) -> Self {
let name = name.into();
if is_role(&name) {
self.reserved.push(name);
return self;
}
match self.by_name.get(&name) {
Some(TokenRef::Length(i)) => self.lengths[*i as usize].1 = px,
_ => {
let i = self.lengths.len() as u16;
self.lengths.push((name.clone(), px));
self.by_name.insert(name, TokenRef::Length(i));
}
}
self
}
pub fn id(&self, name: &str) -> Option<TokenRef> {
self.by_name.get(name).copied()
}
pub fn color_id(&self, name: &str) -> Option<TokenRef> {
self.id(name).filter(|r| r.kind() == TokenKind::Color)
}
pub fn length_id(&self, name: &str) -> Option<TokenRef> {
self.id(name).filter(|r| r.kind() == TokenKind::Length)
}
pub fn colors(&self) -> &[(String, ColorToken)] {
&self.colors
}
pub fn lengths(&self) -> &[(String, f32)] {
&self.lengths
}
pub fn reserved(&self) -> &[String] {
&self.reserved
}
pub fn unresolved(&self) -> &[Unresolved] {
&self.unresolved
}
pub fn is_empty(&self) -> bool {
self.colors.is_empty() && self.lengths.is_empty()
}
pub fn resolve_color(&self, i: u16, theme: &Theme) -> Color {
match self.colors[i as usize].1 {
ColorToken::Value { light, dark } => {
if theme.is_dark() {
dark
} else {
light
}
}
ColorToken::Derived { from, ops } => {
let mut c = self.source_color(from, theme);
for step in &self.ops[ops.start as usize..(ops.start + ops.len) as usize] {
c = match *step {
Step::Lift(t) => c.mix(Color::WHITE, t),
Step::Darken(t) => c.mix(Color::BLACK, t),
Step::Raise(t) => theme.raise(c, t),
Step::Alpha(a) => c.with_alpha(a),
Step::Mix(other, t) => c.mix(self.source_color(other, theme), t),
Step::Readable(on, ratio) => {
let on = self.source_color(on, theme);
c.toward_contrast(crate::widgets::readable_on(on), on, ratio, 0.0)
}
};
}
Color::hex(c.to_hex())
}
}
}
fn source_color(&self, r: TokenRef, theme: &Theme) -> Color {
match r {
TokenRef::ColorRole(i) => (THEME_ROLES[i as usize].get)(theme),
TokenRef::Color(i) => self.resolve_color(i, theme),
TokenRef::LengthRole(_) | TokenRef::Length(_) => unreachable!("a colour source"),
}
}
pub fn halves(&self, i: u16, theme: &Theme) -> (Color, Color) {
if let Some(h) = self.colors[i as usize].1.halves() {
return h;
}
let other = Theme::derive(
if theme.is_dark() {
Appearance::Light
} else {
Appearance::Dark
},
Some(theme.accent),
);
let here = self.resolve_color(i, theme);
let there = self.resolve_color(i, &other);
if theme.is_dark() {
(there, here)
} else {
(here, there)
}
}
pub fn recipe(&self, i: u16) -> Option<String> {
let ColorToken::Derived { from, ops } = self.colors[i as usize].1 else {
return None;
};
let name = |r: TokenRef| -> &str {
match r {
TokenRef::ColorRole(i) => THEME_ROLES[i as usize].node,
TokenRef::Color(i) => &self.colors[i as usize].0,
_ => unreachable!("a colour source"),
}
};
let mut out = name(from).to_string();
for step in &self.ops[ops.start as usize..(ops.start + ops.len) as usize] {
out.push_str(" → ");
out.push_str(&match *step {
Step::Lift(t) => format!("lift {t}"),
Step::Darken(t) => format!("darken {t}"),
Step::Raise(t) => format!("raise {t}"),
Step::Alpha(a) => format!("alpha {a}"),
Step::Mix(r, t) => format!("mix {} {t}", name(r)),
Step::Readable(r, ratio) => format!("readable {} {ratio}", name(r)),
});
}
Some(out)
}
}
pub fn is_role(name: &str) -> bool {
role_ref(name).is_some()
}
pub fn role_ref(name: &str) -> Option<TokenRef> {
if let Some(i) = THEME_ROLES
.iter()
.position(|r| r.name == name || r.node == name)
{
return Some(TokenRef::ColorRole(i as u16));
}
METRIC_ROLES
.iter()
.position(|r| r.name == name || r.node == name)
.map(|i| TokenRef::LengthRole(i as u16))
}
#[derive(Clone, Copy)]
pub struct TokenLookup<'a> {
pub(crate) own: Option<&'a Tokens>,
pub(crate) host: Option<&'a Tokens>,
pub(crate) theme: &'a Theme,
pub(crate) metrics: &'a Metrics,
pub(crate) session: Option<&'a crate::session::Session>,
}
impl<'a> TokenLookup<'a> {
fn table(&self) -> Option<&'a Tokens> {
self.own.or(self.host)
}
pub fn resolve(&self, name: &str) -> Option<TokenRef> {
if let Some(r) = role_ref(name) {
return Some(r);
}
if let Some(r) = self.own.and_then(|t| t.id(name)) {
return Some(r);
}
self.host.and_then(|t| t.id(name))
}
pub fn color(&self, name: &str) -> Result<Color, TokenError> {
match self.resolve(name) {
None => Err(TokenError::Unknown(name.to_string())),
Some(r) if r.kind() != TokenKind::Color => Err(TokenError::Kind {
name: name.to_string(),
is: r.kind(),
wanted: TokenKind::Color,
}),
Some(TokenRef::ColorRole(i)) => Ok((THEME_ROLES[i as usize].get)(self.theme)),
Some(TokenRef::Color(i)) => Ok(self.color_in(name, i)),
Some(_) => unreachable!(),
}
}
pub fn length(&self, name: &str) -> Result<f32, TokenError> {
match self.resolve(name) {
None => Err(TokenError::Unknown(name.to_string())),
Some(r) if r.kind() != TokenKind::Length => Err(TokenError::Kind {
name: name.to_string(),
is: r.kind(),
wanted: TokenKind::Length,
}),
Some(TokenRef::LengthRole(i)) => Ok((METRIC_ROLES[i as usize].get)(self.metrics)),
Some(TokenRef::Length(i)) => Ok(self.length_in(name, i)),
Some(_) => unreachable!(),
}
}
fn color_in(&self, name: &str, i: u16) -> Color {
let own = self.own.filter(|t| t.id(name) == Some(TokenRef::Color(i)));
let t = own.or(self.host).expect("resolved in a table");
t.resolve_color(i, self.theme)
}
fn length_in(&self, name: &str, i: u16) -> f32 {
let own = self.own.filter(|t| t.id(name) == Some(TokenRef::Length(i)));
let t = own.or(self.host).expect("resolved in a table");
t.lengths[i as usize].1
}
pub fn color_at(&self, index: u32) -> Option<Color> {
if index < COLOR_ROLES as u32 {
return Some((THEME_ROLES[index as usize].get)(self.theme));
}
let i = index - COLOR_ROLES as u32;
self.table()
.filter(|t| (i as usize) < t.colors.len())
.map(|t| t.resolve_color(i as u16, self.theme))
}
pub fn length_at(&self, index: u32) -> Option<f32> {
if index < LENGTH_ROLES as u32 {
return Some((METRIC_ROLES[index as usize].get)(self.metrics));
}
let i = (index - LENGTH_ROLES as u32) as usize;
self.table().and_then(|t| t.lengths.get(i)).map(|(_, v)| *v)
}
pub fn colors(&self) -> Vec<(&'a str, Color)> {
let mut out: Vec<(&'a str, Color)> = Vec::new();
for t in [self.host, self.own].into_iter().flatten() {
for (i, (name, _)) in t.colors.iter().enumerate() {
if t.id(name) != Some(TokenRef::Color(i as u16)) {
continue;
}
let c = t.resolve_color(i as u16, self.theme);
match out.iter_mut().find(|(n, _)| *n == name.as_str()) {
Some(slot) => slot.1 = c,
None => out.push((name.as_str(), c)),
}
}
}
out
}
pub fn lengths(&self) -> Vec<(&'a str, f32)> {
let mut out: Vec<(&'a str, f32)> = Vec::new();
for t in [self.host, self.own].into_iter().flatten() {
for (i, (name, v)) in t.lengths.iter().enumerate() {
if t.id(name) != Some(TokenRef::Length(i as u16)) {
continue;
}
match out.iter_mut().find(|(n, _)| *n == name.as_str()) {
Some(slot) => slot.1 = *v,
None => out.push((name.as_str(), *v)),
}
}
}
out
}
pub fn color_names(&self, c: Color) -> Vec<&'a str> {
self.colors()
.into_iter()
.filter(|(_, v)| *v == c)
.map(|(n, _)| n)
.collect()
}
pub fn length_names(&self, v: f32) -> Vec<&'a str> {
self.lengths()
.into_iter()
.filter(|(_, x)| *x == v)
.map(|(n, _)| n)
.collect()
}
}
pub fn reference(s: &str) -> Option<&str> {
s.strip_prefix('$').filter(|n| !n.is_empty())
}
pub struct NameRefs<'a> {
look: TokenLookup<'a>,
missed: Vec<TokenError>,
missed_families: Vec<String>,
}
impl<'a> NameRefs<'a> {
pub fn new(look: TokenLookup<'a>) -> Self {
Self {
look,
missed: Vec::new(),
missed_families: Vec::new(),
}
}
pub fn family(&mut self, name: &str) -> crate::spec::FontFamily {
use crate::spec::FontFamily;
if let Some(stock) = FontFamily::ALL.iter().find(|f| f.name() == Some(name)) {
return *stock;
}
match self.look.session.and_then(|s| s.register_family(name)) {
Some(id) => FontFamily::Custom(id),
None => {
if !self.missed_families.iter().any(|m| m == name) {
self.missed_families.push(name.to_string());
}
FontFamily::Sans
}
}
}
pub fn take_missed_families(&mut self) -> Vec<String> {
std::mem::take(&mut self.missed_families)
}
pub fn lookup(&self) -> TokenLookup<'a> {
self.look
}
pub fn color(&mut self, name: &str) -> Option<Color> {
match self.look.color(name) {
Ok(c) => Some(c),
Err(e) => {
self.missed.push(e);
None
}
}
}
pub fn length(&mut self, name: &str) -> Option<f32> {
match self.look.length(name) {
Ok(v) => Some(v),
Err(e) => {
self.missed.push(e);
None
}
}
}
pub fn color_ref(&mut self, v: &crate::value::Value) -> Option<Option<Color>> {
let name = v.as_str().and_then(reference)?;
Some(self.color(name))
}
pub fn length_ref(&mut self, v: &crate::value::Value) -> Option<Option<f32>> {
let name = v.as_str().and_then(reference)?;
Some(self.length(name))
}
pub fn take_missed(&mut self) -> Vec<TokenError> {
std::mem::take(&mut self.missed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_role_name_is_refused_and_remembered() {
let t = Tokens::new()
.color("surface", Color::WHITE)
.length("radius", 3.0)
.color("peach", Color::WHITE);
assert_eq!(t.reserved(), ["surface", "radius"]);
assert_eq!(t.colors().len(), 1);
assert!(t.lengths().is_empty());
assert!(is_role("border_strong"));
assert!(is_role("borderStrong"));
assert!(is_role("controlPadX"));
}
#[test]
fn redeclaring_a_name_keeps_its_index() {
let t = Tokens::new()
.color("a", Color::WHITE)
.color("b", Color::BLACK)
.color("a", Color::BLACK);
assert_eq!(t.id("a"), Some(TokenRef::Color(0)));
assert_eq!(t.colors()[0].1, ColorToken::same(Color::BLACK));
assert_eq!(t.colors().len(), 2);
}
#[test]
fn a_name_declared_twice_lists_once() {
let t = Tokens::new().color("gap", Color::WHITE).length("gap", 6.0);
let theme = Theme::default();
let metrics = Metrics::default();
let look = TokenLookup {
own: Some(&t),
host: None,
theme: &theme,
metrics: &metrics,
session: None,
};
assert_eq!(look.colors(), vec![]);
assert_eq!(look.lengths(), vec![("gap", 6.0)]);
assert_eq!(look.color_names(Color::WHITE), Vec::<&str>::new());
assert_eq!(
look.color_at(COLOR_ROLES as u32),
Some(Color::WHITE),
"by index still"
);
}
#[test]
fn wire_indices_put_the_roles_first() {
assert_eq!(TokenRef::ColorRole(1).index(), 1);
assert_eq!(TokenRef::Color(0).index(), COLOR_ROLES);
assert_eq!(TokenRef::Length(2).index(), LENGTH_ROLES + 2);
}
#[test]
fn a_reference_is_a_dollar_and_a_name() {
assert_eq!(reference("$peach"), Some("peach"));
assert_eq!(reference("$"), None);
assert_eq!(reference("#fff"), None);
}
const PEACH: Color = Color {
r: 1.0,
g: 0.8,
b: 0.6,
a: 1.0,
};
fn same(a: Color, b: Color) -> bool {
a.to_hex() == b.to_hex()
}
fn q(c: Color) -> Color {
Color::hex(c.to_hex())
}
#[test]
fn a_chain_folds_in_declaration_order() {
let t = Tokens::new()
.color("peach", PEACH)
.color("black", Color::BLACK)
.derive("lit", "peach", [ColorOp::Lift(0.3)])
.derive("wash", "peach", [ColorOp::Lift(0.3), ColorOp::Alpha(0.5)])
.derive(
"lit_then_read",
"peach",
[ColorOp::Lift(0.3), ColorOp::Readable("black".into(), 4.5)],
)
.derive(
"read_then_lit",
"peach",
[ColorOp::Readable("black".into(), 4.5), ColorOp::Lift(0.3)],
);
let theme = Theme::dark();
let hover = t.resolve_color(2, &theme);
assert!(same(hover, PEACH.mix(Color::WHITE, 0.3)));
let wash = t.resolve_color(3, &theme);
assert!(same(wash, q(PEACH.mix(Color::WHITE, 0.3)).with_alpha(0.5)));
let a = t.resolve_color(4, &theme);
let b = t.resolve_color(5, &theme);
assert!(same(a, PEACH.mix(Color::WHITE, 0.3)));
assert!(same(b, PEACH.mix(Color::WHITE, 0.3)));
assert_eq!(t.recipe(3).as_deref(), Some("peach → lift 0.3 → alpha 0.5"));
assert_eq!(t.recipe(0), None);
}
#[test]
fn a_derived_token_follows_the_appearance_with_its_source() {
let t = Tokens::new()
.color_themed("ink", Color::BLACK, Color::WHITE)
.derive("ink_soft", "ink", [ColorOp::Alpha(0.5)])
.derive("accent_up", "accent", [ColorOp::Raise(0.2)])
.derive("alias", "accent", []);
let (light, dark) = (Theme::light(), Theme::dark());
assert!(same(
t.resolve_color(1, &light),
Color::BLACK.with_alpha(0.5)
));
assert!(same(
t.resolve_color(1, &dark),
Color::WHITE.with_alpha(0.5)
));
assert!(same(
t.resolve_color(2, &dark),
dark.accent.mix(Color::WHITE, 0.2)
));
assert!(same(
t.resolve_color(2, &light),
light.accent.mix(Color::BLACK, 0.2)
));
assert_eq!(t.resolve_color(3, &dark), dark.accent);
assert_eq!(t.recipe(2).as_deref(), Some("accent → raise 0.2"));
let (l, d) = t.halves(1, &dark);
assert_eq!((l.to_hex() & 0xff, d.to_hex() & 0xff), (0x80, 0x80));
assert_eq!((l.r, d.r), (0.0, 1.0));
}
#[test]
fn sources_chain_and_readable_reaches() {
let t = Tokens::new()
.color("peach", PEACH)
.color("white", Color::WHITE)
.derive("lit", "peach", [ColorOp::Lift(0.3)])
.derive("lit2", "lit", [ColorOp::Lift(0.35)])
.derive("halfway", "peach", [ColorOp::Mix("white".into(), 0.5)])
.derive(
"on_white",
"peach",
[ColorOp::Readable("white".into(), 4.5)],
);
let theme = Theme::dark();
let pressed = t.resolve_color(3, &theme);
assert!(same(
pressed,
q(PEACH.mix(Color::WHITE, 0.3)).mix(Color::WHITE, 0.35)
));
assert!(same(
t.resolve_color(4, &theme),
PEACH.mix(Color::WHITE, 0.5)
));
let on_white = t.resolve_color(5, &theme);
assert!(
on_white.contrast(Color::WHITE) >= 4.5,
"moved toward black until it read"
);
assert_eq!(t.recipe(5).as_deref(), Some("peach → readable white 4.5"));
}
#[test]
fn an_unresolved_source_drops_the_token_and_is_reported() {
let t = Tokens::new()
.color("peach", PEACH)
.length("gap", 6.0)
.derive("a", "peech", [])
.derive("b", "gap", [])
.derive("c", "later", [])
.color("later", Color::WHITE)
.derive("d", "peach", [ColorOp::Mix("nothing".into(), 0.5)])
.derive("peach", "peach", [ColorOp::Lift(0.1)]);
assert_eq!(t.colors().len(), 2, "peach and later");
assert_eq!(t.id("a"), None);
assert_eq!(
t.colors()[0].1,
ColorToken::same(PEACH),
"peach is not its own lift"
);
let dropped: Vec<(&str, &str)> = t
.unresolved()
.iter()
.map(|u| (u.token.as_str(), u.source.as_str()))
.collect();
assert_eq!(
dropped,
[
("a", "peech"),
("b", "gap"),
("c", "later"),
("d", "nothing"),
("peach", "peach")
]
);
}
#[test]
fn a_derived_token_resolves_through_the_lookup() {
let t = Tokens::new()
.color("peach", PEACH)
.color("lit", Color::BLACK)
.derive("lit", "peach", [ColorOp::Lift(0.3)]);
assert_eq!(t.id("lit"), Some(TokenRef::Color(1)));
assert!(t.colors()[1].1.is_derived());
let theme = Theme::dark();
let metrics = Metrics::default();
let look = TokenLookup {
own: Some(&t),
host: None,
theme: &theme,
metrics: &metrics,
session: None,
};
let want = PEACH.mix(Color::WHITE, 0.3);
assert!(same(look.color("lit").unwrap(), want));
assert!(same(look.color_at(COLOR_ROLES as u32 + 1).unwrap(), want));
assert_eq!(look.color_names(q(want)), vec!["lit"]);
}
#[test]
fn a_verb_parses_with_the_operands_it_takes() {
assert_eq!(ColorOp::parse("lift", None, 0.3), Some(ColorOp::Lift(0.3)));
assert_eq!(
ColorOp::parse("mix", Some("ink"), 0.5),
Some(ColorOp::Mix("ink".into(), 0.5))
);
assert_eq!(ColorOp::parse("lift", Some("ink"), 0.3), None);
assert_eq!(ColorOp::parse("mix", None, 0.5), None);
assert_eq!(ColorOp::parse("glow", None, 0.5), None);
assert_eq!(ColorOp::takes_color("readable"), Some(true));
assert_eq!(ColorOp::takes_color("glow"), None);
assert_eq!(ColorOp::VERBS.iter().position(|v| *v == "alpha"), Some(3));
}
}