use std::collections::HashMap;
use crate::tcss::matcher::MatchedRule;
use crate::tcss::property::PropertyName;
use crate::tcss::value::CssValue;
use crate::tcss::variable::VariableEnvironment;
#[derive(Clone, Debug, Default)]
pub struct ComputedStyle {
properties: HashMap<PropertyName, CssValue>,
}
impl ComputedStyle {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, prop: &PropertyName) -> Option<&CssValue> {
self.properties.get(prop)
}
pub fn set(&mut self, prop: PropertyName, value: CssValue) {
self.properties.insert(prop, value);
}
pub fn has(&self, prop: &PropertyName) -> bool {
self.properties.contains_key(prop)
}
pub fn len(&self) -> usize {
self.properties.len()
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&PropertyName, &CssValue)> {
self.properties.iter()
}
pub fn resolve_variables(&mut self, env: &VariableEnvironment) {
let resolved: Vec<(PropertyName, CssValue)> = self
.properties
.iter()
.filter_map(|(prop, value)| {
if let CssValue::Variable(name) = value {
env.resolve(name).map(|v| (prop.clone(), v.clone()))
} else {
None
}
})
.collect();
for (prop, value) in resolved {
self.properties.insert(prop, value);
}
}
pub fn has_unresolved_variables(&self) -> bool {
self.properties
.values()
.any(|v| matches!(v, CssValue::Variable(_)))
}
}
pub struct CascadeResolver;
type CascadeEntry = (PropertyName, CssValue, (u16, u16, u16), usize);
impl CascadeResolver {
pub fn resolve(matches: &[MatchedRule]) -> ComputedStyle {
let mut normal: Vec<CascadeEntry> = Vec::new();
let mut important: Vec<CascadeEntry> = Vec::new();
for matched in matches {
for decl in &matched.declarations {
let entry = (
decl.property.clone(),
decl.value.clone(),
matched.specificity,
matched.source_order,
);
if decl.important {
important.push(entry);
} else {
normal.push(entry);
}
}
}
normal.sort_by_key(|&(_, _, spec, order)| (spec, order));
important.sort_by_key(|&(_, _, spec, order)| (spec, order));
let mut style = ComputedStyle::new();
for (prop, value, _, _) in normal {
style.set(prop, value);
}
for (prop, value, _, _) in important {
style.set(prop, value);
}
style
}
pub fn resolve_with_variables(
matches: &[MatchedRule],
env: &VariableEnvironment,
) -> ComputedStyle {
let mut style = Self::resolve(matches);
style.resolve_variables(env);
style
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Color;
use crate::color::NamedColor;
use crate::tcss::property::Declaration;
use crate::tcss::value::Length;
use crate::tcss::variable::VariableEnvironment;
fn matched_rule(
specificity: (u16, u16, u16),
source_order: usize,
declarations: Vec<Declaration>,
) -> MatchedRule {
MatchedRule {
specificity,
source_order,
declarations,
}
}
#[test]
fn empty_matches_empty_style() {
let style = CascadeResolver::resolve(&[]);
assert!(style.is_empty());
assert_eq!(style.len(), 0);
}
#[test]
fn single_rule_applied() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Color(Color::Named(NamedColor::Red)),
)],
)];
let style = CascadeResolver::resolve(&rules);
assert_eq!(style.len(), 1);
assert!(style.has(&PropertyName::Color));
}
#[test]
fn later_rule_overrides() {
let rules = vec![
matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("red".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("blue".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("blue".into()))
);
}
#[test]
fn higher_specificity_wins() {
let rules = vec![
matched_rule(
(0, 1, 0),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("class-wins".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("type-loses".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("class-wins".into()))
);
}
#[test]
fn important_overrides_specificity() {
let rules = vec![
matched_rule(
(1, 0, 0),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("high-spec".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::important(
PropertyName::Color,
CssValue::Keyword("important-wins".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("important-wins".into()))
);
}
#[test]
fn important_vs_important() {
let rules = vec![
matched_rule(
(0, 1, 0),
0,
vec![Declaration::important(
PropertyName::Color,
CssValue::Keyword("class-important".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::important(
PropertyName::Color,
CssValue::Keyword("type-important".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("class-important".into()))
);
}
#[test]
fn multiple_properties_merged() {
let rules = vec![
matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("red".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::new(
PropertyName::Background,
CssValue::Keyword("blue".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(style.len(), 2);
assert!(style.has(&PropertyName::Color));
assert!(style.has(&PropertyName::Background));
}
#[test]
fn same_property_last_wins() {
let rules = vec![
matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("first".into()),
)],
),
matched_rule(
(0, 0, 1),
1,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("second".into()),
)],
),
matched_rule(
(0, 0, 1),
2,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("third".into()),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("third".into()))
);
}
#[test]
fn computed_style_accessors() {
let mut style = ComputedStyle::new();
assert!(style.is_empty());
assert_eq!(style.len(), 0);
assert!(!style.has(&PropertyName::Color));
assert!(style.get(&PropertyName::Color).is_none());
style.set(PropertyName::Color, CssValue::Keyword("red".into()));
assert!(!style.is_empty());
assert_eq!(style.len(), 1);
assert!(style.has(&PropertyName::Color));
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("red".into()))
);
}
#[test]
fn computed_style_iteration() {
let mut style = ComputedStyle::new();
style.set(PropertyName::Color, CssValue::Keyword("red".into()));
style.set(PropertyName::Width, CssValue::Length(Length::Cells(10)));
let pairs: Vec<_> = style.iter().collect();
assert_eq!(pairs.len(), 2);
}
#[test]
fn resolve_with_no_variables() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Color(Color::Named(NamedColor::Red)),
)],
)];
let env = VariableEnvironment::new();
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Color(Color::Named(NamedColor::Red)))
);
}
#[test]
fn resolve_variable_from_global() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Variable("fg".into()),
)],
)];
let mut env = VariableEnvironment::new();
env.set_global("fg", CssValue::Color(Color::Named(NamedColor::White)));
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Color(Color::Named(NamedColor::White)))
);
}
#[test]
fn resolve_variable_from_theme() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Variable("fg".into()),
)],
)];
let mut env = VariableEnvironment::new();
env.set_global("fg", CssValue::Color(Color::Named(NamedColor::White)));
env.set_theme("fg", CssValue::Color(Color::Named(NamedColor::Red)));
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Color(Color::Named(NamedColor::Red)))
);
}
#[test]
fn resolve_variable_missing_stays_variable() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![Declaration::new(
PropertyName::Color,
CssValue::Variable("missing".into()),
)],
)];
let env = VariableEnvironment::new();
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Variable("missing".into()))
);
}
#[test]
fn resolve_multiple_variables() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![
Declaration::new(PropertyName::Color, CssValue::Variable("fg".into())),
Declaration::new(PropertyName::Background, CssValue::Variable("bg".into())),
],
)];
let mut env = VariableEnvironment::new();
env.set_global("fg", CssValue::Color(Color::Named(NamedColor::White)));
env.set_global("bg", CssValue::Color(Color::Named(NamedColor::Black)));
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Color(Color::Named(NamedColor::White)))
);
assert_eq!(
style.get(&PropertyName::Background),
Some(&CssValue::Color(Color::Named(NamedColor::Black)))
);
}
#[test]
fn resolve_mixed_variables_and_concrete() {
let rules = vec![matched_rule(
(0, 0, 1),
0,
vec![
Declaration::new(PropertyName::Color, CssValue::Variable("fg".into())),
Declaration::new(PropertyName::Width, CssValue::Length(Length::Cells(20))),
],
)];
let mut env = VariableEnvironment::new();
env.set_global("fg", CssValue::Color(Color::Named(NamedColor::Red)));
let style = CascadeResolver::resolve_with_variables(&rules, &env);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Color(Color::Named(NamedColor::Red)))
);
assert_eq!(
style.get(&PropertyName::Width),
Some(&CssValue::Length(Length::Cells(20)))
);
}
#[test]
fn has_unresolved_true() {
let mut style = ComputedStyle::new();
style.set(PropertyName::Color, CssValue::Variable("fg".into()));
assert!(style.has_unresolved_variables());
}
#[test]
fn has_unresolved_false() {
let mut style = ComputedStyle::new();
style.set(
PropertyName::Color,
CssValue::Color(Color::Named(NamedColor::Red)),
);
assert!(!style.has_unresolved_variables());
}
#[test]
fn has_unresolved_empty() {
let style = ComputedStyle::new();
assert!(!style.has_unresolved_variables());
}
#[test]
fn real_cascade_example() {
let rules = vec![
matched_rule(
(0, 0, 1),
0,
vec![
Declaration::new(PropertyName::Color, CssValue::Keyword("white".into())),
Declaration::new(PropertyName::TextStyle, CssValue::Keyword("bold".into())),
],
),
matched_rule(
(0, 1, 0),
1,
vec![Declaration::new(
PropertyName::Color,
CssValue::Keyword("red".into()),
)],
),
matched_rule(
(1, 0, 0),
2,
vec![Declaration::new(
PropertyName::Width,
CssValue::Length(Length::Cells(30)),
)],
),
];
let style = CascadeResolver::resolve(&rules);
assert_eq!(style.len(), 3);
assert_eq!(
style.get(&PropertyName::Color),
Some(&CssValue::Keyword("red".into()))
);
assert_eq!(
style.get(&PropertyName::TextStyle),
Some(&CssValue::Keyword("bold".into()))
);
assert_eq!(
style.get(&PropertyName::Width),
Some(&CssValue::Length(Length::Cells(30)))
);
}
}