use crate::widget::WidgetKind;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Selector {
Kind(WidgetKind),
Class(String),
Id(String),
State(PseudoState),
#[default]
Universal,
And(Vec<Selector>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PseudoState {
Normal,
Hover,
Pressed,
Disabled,
Focused,
Checked,
Selected,
}
#[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(),
}
}
pub fn matches(
&self,
kind: WidgetKind,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
) -> bool {
match self {
Selector::Kind(k) => *k == kind,
Selector::Class(c) => class == Some(c.as_str()),
Selector::Id(i) => id == Some(i.as_str()),
Selector::State(s) => state == Some(*s),
Selector::Universal => true,
Selector::And(selectors) => selectors.iter().all(|s| s.matches(kind, class, id, state)),
}
}
}
#[derive(Debug, Clone)]
pub struct StyleSheet {
rules: Vec<StyleRule>,
}
impl StyleSheet {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add_rule(&mut self, rule: StyleRule) {
self.rules.push(rule);
self.rules.sort_by_key(|r| r.specificity);
}
pub fn match_rules(
&self,
kind: WidgetKind,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
) -> Vec<&StyleRule> {
self.rules.iter().filter(|r| r.selector.matches(kind, class, id, state)).collect()
}
pub fn rules(&self) -> &[StyleRule] {
&self.rules
}
}
impl Default for StyleSheet {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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 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 stylesheet_empty_rules() {
let ss = StyleSheet::new();
assert!(ss.rules().is_empty());
}
}