use crate::compat::{Box, HashMap, String, Vec};
use crate::widget::WidgetKind;
use core::cell::RefCell;
use crate::style::theme_state::WidgetState;
#[derive(Debug, Clone, Copy)]
pub(crate) enum RuleIndexKey<'a> {
Kind(&'a str),
Class(&'a str),
Id(&'a str),
Any,
}
fn selector_index_key(selector: &Selector) -> RuleIndexKey<'_> {
match selector {
Selector::Kind(kind) => RuleIndexKey::Kind(kind.css_name()),
Selector::Class(class) => RuleIndexKey::Class(class),
Selector::Id(id) => RuleIndexKey::Id(id),
Selector::State(_) | Selector::Universal => RuleIndexKey::Any,
Selector::And(parts) => {
if let Some(id) = parts.iter().find_map(|part| match part {
Selector::Id(id) => Some(id.as_str()),
_ => None,
}) {
return RuleIndexKey::Id(id);
}
if let Some(class) = parts.iter().find_map(|part| match part {
Selector::Class(class) => Some(class.as_str()),
_ => None,
}) {
return RuleIndexKey::Class(class);
}
if let Some(kind) = parts.iter().find_map(|part| match part {
Selector::Kind(kind) => Some(kind.css_name()),
_ => None,
}) {
return RuleIndexKey::Kind(kind);
}
RuleIndexKey::Any
}
Selector::Descendant { subject, .. } | Selector::Child { subject, .. } => {
selector_index_key(subject)
}
}
}
fn normalize_kind_key(input: &str, out: &mut String) {
out.clear();
for ch in input.chars() {
if matches!(ch, '_' | '-' | ' ') {
continue;
}
out.extend(ch.to_lowercase());
}
}
#[derive(Default)]
pub(crate) struct MatchScratch {
pub(crate) positions: Vec<usize>,
kind_key: String,
}
pub(crate) fn with_match_scratch<R>(f: impl FnOnce(&mut MatchScratch) -> R) -> R {
thread_local! {
static SCRATCH: RefCell<MatchScratch> = RefCell::new(MatchScratch::default());
}
SCRATCH.with(|cell| {
let mut borrowed = cell.try_borrow_mut().ok();
let mut fallback = MatchScratch::default();
let scratch: &mut MatchScratch = borrowed.as_deref_mut().unwrap_or(&mut fallback);
f(scratch)
})
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RuleIndex {
by_kind: HashMap<String, Vec<usize>>,
by_class: HashMap<String, Vec<usize>>,
by_id: HashMap<String, Vec<usize>>,
any: Vec<usize>,
}
impl RuleIndex {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn clear(&mut self) {
self.by_kind.clear();
self.by_class.clear();
self.by_id.clear();
self.any.clear();
}
pub(crate) fn insert(&mut self, position: usize, key: RuleIndexKey<'_>) {
match key {
RuleIndexKey::Kind(kind) => {
let mut normalized = String::new();
normalize_kind_key(kind, &mut normalized);
self.by_kind.entry(normalized).or_default().push(position);
}
RuleIndexKey::Class(class) => {
self.by_class.entry(String::from(class)).or_default().push(position);
}
RuleIndexKey::Id(id) => {
self.by_id.entry(String::from(id)).or_default().push(position);
}
RuleIndexKey::Any => self.any.push(position),
}
}
pub(crate) fn candidates_into(
&self,
kind: &str,
class: Option<&str>,
id: Option<&str>,
scratch: &mut MatchScratch,
) {
normalize_kind_key(kind, &mut scratch.kind_key);
let kind_bucket =
self.by_kind.get(scratch.kind_key.as_str()).map(Vec::as_slice).unwrap_or(&[]);
let class_bucket =
class.and_then(|class| self.by_class.get(class)).map(Vec::as_slice).unwrap_or(&[]);
let id_bucket = id.and_then(|id| self.by_id.get(id)).map(Vec::as_slice).unwrap_or(&[]);
merge_buckets([kind_bucket, class_bucket, id_bucket, &self.any], &mut scratch.positions);
}
}
fn merge_buckets<const N: usize>(buckets: [&[usize]; N], out: &mut Vec<usize>) {
out.clear();
let mut cursors = [0usize; N];
loop {
let mut best: Option<(usize, usize)> = None;
for (index, bucket) in buckets.iter().enumerate() {
let Some(&position) = bucket.get(cursors[index]) else {
continue;
};
if best.is_none_or(|(current, _)| position < current) {
best = Some((position, index));
}
}
let Some((position, index)) = best else {
break;
};
out.push(position);
cursors[index] += 1;
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Selector {
Kind(WidgetKind),
Class(String),
Id(String),
State(PseudoState),
#[default]
Universal,
And(Vec<Selector>),
Descendant {
ancestor: Box<Selector>,
subject: Box<Selector>,
},
Child {
parent: Box<Selector>,
subject: Box<Selector>,
},
}
pub type PseudoState = WidgetState;
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: Selector,
pub name: String,
pub specificity: u32,
}
impl StyleRule {
pub fn new(selector: Selector, name: impl Into<String>) -> Self {
let name = name.into();
let specificity = selector.specificity();
Self { selector, name, specificity }
}
}
impl Selector {
pub fn specificity(&self) -> u32 {
match self {
Selector::Kind(_) => 1,
Selector::Class(_) => 10,
Selector::Id(_) => 100,
Selector::State(_) => 1,
Selector::Universal => 0,
Selector::And(selectors) => selectors.iter().map(|s| s.specificity()).sum(),
Selector::Descendant { ancestor, subject }
| Selector::Child { parent: ancestor, subject } => {
ancestor.specificity() + subject.specificity()
}
}
}
pub fn matches(
&self,
kind: WidgetKind,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
) -> bool {
self.matches_in(ElementRef { kind, class, id, state }, &[])
}
pub fn matches_in(&self, element: ElementRef<'_>, ancestors: &[ElementRef<'_>]) -> bool {
match self {
Selector::Kind(k) => *k == element.kind,
Selector::Class(c) => element.class == Some(c.as_str()),
Selector::Id(i) => element.id == Some(i.as_str()),
Selector::State(s) => element.state == Some(*s),
Selector::Universal => true,
Selector::And(selectors) => selectors.iter().all(|s| s.matches_in(element, ancestors)),
Selector::Descendant { ancestor, subject } => {
subject.matches_in(element, ancestors)
&& ancestors.iter().any(|candidate| ancestor.matches_in(*candidate, &[]))
}
Selector::Child { parent, subject } => {
subject.matches_in(element, ancestors)
&& ancestors.last().is_some_and(|candidate| parent.matches_in(*candidate, &[]))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementRef<'a> {
pub kind: WidgetKind,
pub class: Option<&'a str>,
pub id: Option<&'a str>,
pub state: Option<PseudoState>,
}
#[derive(Debug, Clone)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
index: RuleIndex,
}
impl StyleSheet {
pub fn new() -> Self {
Self { rules: Vec::new(), index: RuleIndex::new() }
}
pub fn add_rule(&mut self, rule: StyleRule) {
self.rules.push(rule);
self.rules.sort_by_key(|r| r.specificity);
self.rebuild_index();
}
fn rebuild_index(&mut self) {
self.index.clear();
for (position, rule) in self.rules.iter().enumerate() {
self.index.insert(position, selector_index_key(&rule.selector));
}
}
pub fn match_rules(
&self,
kind: WidgetKind,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
) -> Vec<&StyleRule> {
with_match_scratch(|scratch| {
self.index.candidates_into(kind.css_name(), class, id, scratch);
scratch
.positions
.iter()
.filter_map(|&position| self.rules.get(position))
.filter(|rule| rule.selector.matches(kind, class, id, state))
.collect()
})
}
pub fn match_rules_in(
&self,
element: ElementRef<'_>,
ancestors: &[ElementRef<'_>],
) -> Vec<&StyleRule> {
with_match_scratch(|scratch| {
self.index.candidates_into(element.kind.css_name(), element.class, element.id, scratch);
scratch
.positions
.iter()
.filter_map(|&position| self.rules.get(position))
.filter(|rule| rule.selector.matches_in(element, ancestors))
.collect()
})
}
pub fn rules(&self) -> &[StyleRule] {
&self.rules
}
}
crate::impl_default_via_new!(StyleSheet);
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::MiniToString;
#[test]
fn selector_universal_matches_anything() {
assert!(Selector::Universal.matches(WidgetKind::Button, None, None, None));
}
#[test]
fn selector_kind_matches_correct_kind() {
let s = Selector::Kind(WidgetKind::Button);
assert!(s.matches(WidgetKind::Button, None, None, None));
assert!(!s.matches(WidgetKind::Label, None, None, None));
}
#[test]
fn selector_class_matches() {
let s = Selector::Class("primary".to_string());
assert!(s.matches(WidgetKind::Button, Some("primary"), None, None));
assert!(!s.matches(WidgetKind::Button, Some("secondary"), None, None));
assert!(!s.matches(WidgetKind::Button, None, None, None));
}
#[test]
fn selector_id_matches() {
let s = Selector::Id("submit-btn".to_string());
assert!(s.matches(WidgetKind::Button, None, Some("submit-btn"), None));
assert!(!s.matches(WidgetKind::Button, None, Some("other"), None));
}
#[test]
fn selector_state_matches() {
let s = Selector::State(PseudoState::Hover);
assert!(s.matches(WidgetKind::Button, None, None, Some(PseudoState::Hover)));
assert!(!s.matches(WidgetKind::Button, None, None, Some(PseudoState::Normal)));
}
#[test]
fn the_css_state_name_is_the_theme_state() {
let from_theme: PseudoState = WidgetState::Active;
assert_eq!(from_theme.state_suffix(), "active");
assert_eq!(PseudoState::from_state_suffix("hover"), Some(WidgetState::Hover));
assert_eq!(PseudoState::from_state_suffix("disabled"), Some(WidgetState::Disabled));
assert_eq!(PseudoState::from_state_suffix("focus"), Some(WidgetState::Focused));
}
#[test]
fn selector_and_combines() {
let s = Selector::And(vec![
Selector::Kind(WidgetKind::Button),
Selector::Class("primary".to_string()),
Selector::State(PseudoState::Hover),
]);
assert!(s.matches(WidgetKind::Button, Some("primary"), None, Some(PseudoState::Hover)));
assert!(!s.matches(WidgetKind::Button, Some("secondary"), None, Some(PseudoState::Hover)));
assert!(!s.matches(WidgetKind::Label, Some("primary"), None, Some(PseudoState::Hover)));
}
#[test]
fn specificity_kind_is_1() {
assert_eq!(Selector::Kind(WidgetKind::Button).specificity(), 1);
}
#[test]
fn specificity_class_is_10() {
assert_eq!(Selector::Class("btn".to_string()).specificity(), 10);
}
#[test]
fn specificity_id_is_100() {
assert_eq!(Selector::Id("main".to_string()).specificity(), 100);
}
#[test]
fn specificity_and_sums() {
let s = Selector::And(vec![
Selector::Kind(WidgetKind::Button),
Selector::Class("primary".to_string()),
]);
assert_eq!(s.specificity(), 11);
}
#[test]
fn stylesheet_add_rule_and_match() {
let mut ss = StyleSheet::new();
ss.add_rule(StyleRule::new(Selector::Kind(WidgetKind::Button), "button"));
ss.add_rule(StyleRule::new(Selector::Class("primary".to_string()), "primary"));
let matched = ss.match_rules(WidgetKind::Button, Some("primary"), None, None);
assert_eq!(matched.len(), 2);
}
#[test]
fn candidates_from_several_buckets_come_back_in_rule_order() {
let mut ss = StyleSheet::new();
ss.add_rule(StyleRule::new(Selector::Kind(WidgetKind::Button), "kind"));
ss.add_rule(StyleRule::new(Selector::Universal, "universal"));
ss.add_rule(StyleRule::new(
Selector::And(vec![
Selector::Kind(WidgetKind::Button),
Selector::Class("primary".to_string()),
]),
"compound",
));
ss.add_rule(StyleRule::new(Selector::Class("primary".to_string()), "class"));
let names: Vec<&str> = ss
.match_rules(WidgetKind::Button, Some("primary"), None, None)
.iter()
.map(|rule| rule.name.as_str())
.collect();
assert_eq!(
names,
vec!["universal", "kind", "class", "compound"],
"specificity ascending (0, 1, 10, 11) is the cascade order"
);
}
#[test]
fn the_indexed_match_returns_every_rule_the_linear_scan_would() {
let mut ss = StyleSheet::new();
ss.add_rule(StyleRule::new(Selector::Universal, "universal"));
ss.add_rule(StyleRule::new(
Selector::And(vec![
Selector::Kind(WidgetKind::Button),
Selector::Class("primary".to_string()),
]),
"button.primary",
));
ss.add_rule(StyleRule::new(Selector::Id("submit".to_string()), "id"));
ss.add_rule(StyleRule::new(
Selector::Descendant {
ancestor: Box::new(Selector::Kind(WidgetKind::Panel)),
subject: Box::new(Selector::Class("primary".to_string())),
},
"panel primary",
));
ss.add_rule(StyleRule::new(Selector::Kind(WidgetKind::Label), "label"));
let matched = ss.match_rules(WidgetKind::Button, Some("primary"), Some("submit"), None);
let names: Vec<&str> = matched.iter().map(|rule| rule.name.as_str()).collect();
assert_eq!(names, vec!["universal", "button.primary", "id"]);
let button = ElementRef {
kind: WidgetKind::Button,
class: Some("primary"),
id: Some("submit"),
state: None,
};
let panel = ElementRef { kind: WidgetKind::Panel, ..button };
let matched = ss.match_rules_in(button, &[panel]);
assert!(matched.iter().any(|rule| rule.name == "panel primary"));
assert!(!matched.iter().any(|rule| rule.name == "label"));
}
#[test]
fn stylesheet_empty_rules() {
let ss = StyleSheet::new();
assert!(ss.rules().is_empty());
}
#[test]
fn a_descendant_combinator_matches_at_any_depth() {
let selector = Selector::Descendant {
ancestor: Box::new(Selector::Kind(WidgetKind::Panel)),
subject: Box::new(Selector::Kind(WidgetKind::Button)),
};
let button = ElementRef { kind: WidgetKind::Button, class: None, id: None, state: None };
let panel = ElementRef { kind: WidgetKind::Panel, ..button };
let label = ElementRef { kind: WidgetKind::Label, ..button };
assert!(selector.matches_in(button, &[panel, label]), "the panel is two levels up");
assert!(!selector.matches_in(button, &[label]));
assert!(!selector.matches_in(panel, &[panel]), "the subject itself must be a button");
}
#[test]
fn a_child_combinator_matches_only_the_immediate_parent() {
let selector = Selector::Child {
parent: Box::new(Selector::Kind(WidgetKind::Panel)),
subject: Box::new(Selector::Kind(WidgetKind::Button)),
};
let button = ElementRef { kind: WidgetKind::Button, class: None, id: None, state: None };
let panel = ElementRef { kind: WidgetKind::Panel, ..button };
let label = ElementRef { kind: WidgetKind::Label, ..button };
assert!(selector.matches_in(button, &[panel]), "panel is the immediate parent");
assert!(
!selector.matches_in(button, &[panel, label]),
"a label sits between the panel and the button"
);
}
#[test]
fn a_combinator_without_context_matches_nothing() {
let selector = Selector::Descendant {
ancestor: Box::new(Selector::Universal),
subject: Box::new(Selector::Kind(WidgetKind::Button)),
};
assert!(!selector.matches(WidgetKind::Button, None, None, None));
}
#[test]
fn a_combinator_weights_its_compounds_and_nothing_else() {
let descendant = Selector::Descendant {
ancestor: Box::new(Selector::Kind(WidgetKind::Panel)),
subject: Box::new(Selector::Class("primary".to_string())),
};
let child = Selector::Child {
parent: Box::new(Selector::Kind(WidgetKind::Panel)),
subject: Box::new(Selector::Class("primary".to_string())),
};
assert_eq!(descendant.specificity(), 11);
assert_eq!(child.specificity(), descendant.specificity());
}
}