use std::collections::{HashMap, HashSet}; use std::borrow::Cow; use std::sync::Mutex; use std::hash::{Hash, Hasher}; use super::reactive::ElementId; #[derive(Debug, Clone)] pub enum AttributeSelector { Exists(String), Equals(String, String), } fn parse_attribute(input: &str) -> AttributeSelector { let input = input.trim(); if let Some((name, val)) = input.split_once('=') { let name = name.trim().to_string(); let val = val.trim().trim_matches('"').trim_matches('\'').to_string(); AttributeSelector::Equals(name, val) } else { AttributeSelector::Exists(input.to_string()) } } #[derive(Debug, Clone)] pub struct CompoundSelector { pub tag: Option, pub id: Option, pub classes: Vec, pub pseudo_classes: Vec, pub attributes: Vec, } impl CompoundSelector { fn parse(input: &str) -> Self { let input = input.trim(); let mut tag = None; let mut id = None; let mut classes = Vec::new(); let mut pseudo_classes = Vec::new(); let mut attributes = Vec::new(); let mut current = String::new(); let mut delim: Option = None; for ch in input.chars() { match ch { '#' | '.' | ':' | '[' => { if !current.is_empty() { match (delim, ch) { (Some('.'), _) => classes.push(std::mem::take(&mut current)), (Some(':'), _) => pseudo_classes.push(std::mem::take(&mut current)), (Some('#'), _) => id = Some(std::mem::take(&mut current)), (Some('['), _) => { attributes.push(parse_attribute(&std::mem::take(&mut current))); } _ if tag.is_none() && id.is_none() && classes.is_empty() && pseudo_classes.is_empty() && attributes.is_empty() => { tag = Some(std::mem::take(&mut current)); } _ => current.clear(), } } delim = Some(ch); } ']' => { if delim == Some('[') && !current.is_empty() { attributes.push(parse_attribute(&std::mem::take(&mut current))); delim = None; } } _ => current.push(ch), } } if !current.is_empty() { match delim { Some('#') => id = Some(current), Some('.') => classes.push(current), Some(':') => pseudo_classes.push(current), Some('[') => attributes.push(parse_attribute(¤t)), None => tag = Some(current), _ => {} } } Self { tag, id, classes, pseudo_classes, attributes } } fn specificity(&self) -> (u32, u32, u32) { ( if self.id.is_some() { 1 } else { 0 }, self.classes.len() as u32 + self.attributes.len() as u32 + self.pseudo_classes.len() as u32, if self.tag.is_some() { 1 } else { 0 }, ) } pub fn matches_element( &self, type_name: &str, el_id: Option<&str>, el_classes: &[&str], active_pseudo: &[&str], structural: &StructuralContext, el_attributes: &HashMap, ) -> bool { if let Some(ref t) = self.tag { if t != type_name && t != "*" { return false; } } if let Some(ref i) = self.id { match el_id { Some(eid) if eid == i => {} _ => return false, } } for cls in &self.classes { if !el_classes.contains(&cls.as_str()) { return false; } } for attr in &self.attributes { match attr { AttributeSelector::Exists(name) => { if !el_attributes.contains_key(name) { return false; } } AttributeSelector::Equals(name, val) => { match el_attributes.get(name) { Some(v) if v == val => {} _ => return false, } } } } for pc in &self.pseudo_classes { match pc.as_str() { "first-child" => { if structural.sibling_index != 0 { return false; } } "last-child" => { if structural.sibling_index + 1 != structural.sibling_total { return false; } } "first-of-type" => { if structural.type_index != 0 { return false; } } "empty" => { if structural.has_children { return false; } } "root" => { if !structural.is_root { return false; } } s if s.starts_with("nth-child(") => { let inner = &s[10..s.len().saturating_sub(1)]; if !nth_matches(inner, structural.sibling_index + 1) { return false; } } _ => { if !active_pseudo.contains(&pc.as_str()) { return false; } } } } true } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Combinator { Descendant, // Space Child, // > NextSibling, // + Subsequent, // ~ } #[derive(Debug, Clone)] pub struct ComplexSelector { pub compounds: Vec, pub combinators: Vec, } impl ComplexSelector { pub fn parse(input: &str) -> Self { let input = input.trim(); if input.is_empty() { return Self { compounds: vec![], combinators: vec![] }; } let mut parts: Vec<(String, Combinator)> = Vec::new(); let mut buf = String::new(); let mut chars = input.chars().peekable(); while let Some(ch) = chars.next() { match ch { '>' | '+' | '~' => { let right = buf.trim().to_string(); let combinator = match ch { '>' => Combinator::Child, '+' => Combinator::NextSibling, '~' => Combinator::Subsequent, _ => unreachable!(), }; if !right.is_empty() { parts.push((right, combinator)); } buf.clear(); while let Some(&c) = chars.peek() { if c.is_ascii_whitespace() { chars.next(); } else { break; } } } '#' | '.' | ':' => { buf.push(ch); } c if c.is_ascii_whitespace() => { let candidate = buf.trim().to_string(); if !candidate.is_empty() { let mut peek_pos = chars.clone(); let next_nonws = peek_pos.find(|c| !c.is_ascii_whitespace()); match next_nonws { Some('>') | Some('+') | Some('~') => { buf.push(' '); } _ => { parts.push((candidate, Combinator::Descendant)); buf.clear(); } } } } _ => buf.push(ch), } } let last = buf.trim().to_string(); if !last.is_empty() { if parts.is_empty() { parts.push((last, Combinator::Descendant)); } else { parts.push((last, Combinator::Descendant)); } } let compounds: Vec = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect(); let combinators: Vec = parts.iter().rev().skip(1).rev().map(|(_, c)| *c).collect(); Self { compounds, combinators } } fn check_compound_against(&self, i: usize, info: &AncestorInfo) -> bool { let compound = &self.compounds[i]; let tag_ok = compound.tag.as_ref().map_or(true, |t| { t == &info.type_name || t == "*" }); let id_ok = compound.id.as_ref().map_or(true, |id| { info.id.as_ref().map_or(false, |sid| sid == id) }); let classes_ok = compound.classes.iter() .all(|c| info.classes.contains(c)); tag_ok && id_ok && classes_ok } pub fn matches( &self, type_name: &str, el_id: Option<&str>, el_classes: &[&str], active_pseudo: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], el_attributes: &HashMap, ) -> bool { if self.compounds.is_empty() { return false; } let target = self.compounds.last().unwrap(); if !target.matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) { return false; } if self.compounds.len() == 1 { return true; } let mut ai = 0; let mut si = preceding_siblings.len(); for i in (0..self.compounds.len() - 1).rev() { let combinator = self.combinators[i]; match combinator { Combinator::Descendant => { let mut found = false; for a in &ancestors[ai..] { if self.check_compound_against(i, a) { found = true; break; } } if !found { return false; } } Combinator::Child => { if ai >= ancestors.len() { return false; } let a = &ancestors[ai]; if !self.check_compound_against(i, a) { return false; } ai += 1; } Combinator::NextSibling => { if si == 0 { return false; } let sib = &preceding_siblings[si - 1]; if !self.check_compound_against(i, sib) { return false; } si -= 1; } Combinator::Subsequent => { let mut found = false; for sib in preceding_siblings[..si].iter().rev() { if self.check_compound_against(i, sib) { found = true; break; } } if !found { return false; } } } } true } pub fn specificity(&self) -> (u32, u32, u32) { let mut a = 0u32; let mut b = 0u32; let mut c = 0u32; for compound in &self.compounds { let (sa, sb, sc) = compound.specificity(); a += sa; b += sb; c += sc; } (a, b, c) } pub fn as_simple(&self) -> Option<&CompoundSelector> { if self.compounds.len() == 1 { self.compounds.first() } else { None } } pub fn has_pseudo_class(&self, pc: &str) -> bool { self.compounds.iter().any(|c| c.pseudo_classes.iter().any(|p| p == pc)) } } #[derive(Debug, Clone)] pub struct AncestorInfo { pub type_name: String, pub id: Option, pub classes: Vec, } impl AncestorInfo { pub fn new(type_name: &str, classes: Vec) -> Self { Self { type_name: type_name.to_string(), id: None, classes } } pub fn new_with_id(type_name: &str, id: Option, classes: Vec) -> Self { Self { type_name: type_name.to_string(), id, classes } } } #[derive(Debug, Clone, Default)] pub struct StructuralContext { pub sibling_index: usize, pub sibling_total: usize, pub type_index: usize, pub type_total: usize, pub has_children: bool, pub is_root: bool, } fn nth_matches(expr: &str, n: usize) -> bool { let expr = expr.trim(); if expr.eq_ignore_ascii_case("odd") { return n % 2 == 1; } if expr.eq_ignore_ascii_case("even") { return n % 2 == 0; } if let Ok(num) = expr.parse::() { return n == num as usize; } let expr_lower = expr.to_lowercase(); if let Some(n_pos) = expr_lower.find('n') { let a_str = expr_lower[..n_pos].trim(); let b_str = expr_lower[n_pos + 1..].trim(); let a = if a_str.is_empty() || a_str == "+" { 1 } else if a_str == "-" { -1 } else { a_str.parse::().unwrap_or(0) }; let b = if b_str.is_empty() { 0 } else if b_str.starts_with('+') { b_str[1..].trim().parse::().unwrap_or(0) } else if b_str.starts_with('-') { b_str.parse::().unwrap_or(0) } else { b_str.parse::().unwrap_or(0) }; if a == 0 { return n == b as usize; } let n_i32 = n as i32; if a > 0 { let k = n_i32 - b; k >= 0 && k % a == 0 } else { let diff = b - n_i32; diff >= 0 && diff % a.abs() == 0 } } else { false } } #[derive(Debug, Clone)] pub struct StyleRule { pub selector: ComplexSelector, pub properties: HashMap, } impl StyleRule { pub fn build(selector_str: String, properties: HashMap) -> Self { Self { selector: ComplexSelector::parse(&selector_str), properties, } } } #[derive(Debug, Clone)] pub struct StyleCache { entries: HashMap, max_entries: usize, } impl StyleCache { pub fn new(max_entries: usize) -> Self { Self { entries: HashMap::new(), max_entries } } pub fn get_or_compute( &mut self, type_name: &str, props: &[(Cow<'_, str>, Cow<'_, str>)], epoch: u64, matched_sheets: &[&HashMap], ) -> ComputedStyle { let key = { let mut hasher = std::collections::hash_map::DefaultHasher::new(); type_name.hash(&mut hasher); for (k, v) in props { k.hash(&mut hasher); v.hash(&mut hasher); } epoch.hash(&mut hasher); hasher.finish() }; if let Some(cached) = self.entries.get(&key) { return cached.clone(); } let style = ComputedStyle::compute(props, matched_sheets); if self.entries.len() >= self.max_entries { self.entries.clear(); } self.entries.insert(key, style.clone()); style } pub fn clear(&mut self) { self.entries.clear(); } } #[derive(Debug)] pub struct StyleSheet { rules: Vec, index: Option, epoch: u64, cache: Mutex, } impl Clone for StyleSheet { fn clone(&self) -> Self { Self { rules: self.rules.clone(), index: self.index.clone(), epoch: self.epoch, cache: Mutex::new(StyleCache::new(1024)), } } } impl Default for StyleSheet { fn default() -> Self { Self { rules: Vec::new(), index: None, epoch: 0, cache: Mutex::new(StyleCache::new(1024)), } } } impl StyleSheet { pub fn new() -> Self { Self::default() } pub fn add_rule(&mut self, selector: String, properties: HashMap) { for part in split_selectors(&selector) { self.rules.push(StyleRule::build(part, properties.clone())); } self.index = None; } pub fn build_index(&mut self) { self.epoch += 1; self.cache.lock().unwrap().clear(); let mut index = StyleIndex::new(); index.epoch = self.epoch; for (i, rule) in self.rules.iter().enumerate() { let specificity = rule.selector.specificity(); index.rule_specificities.push(specificity); index.rules.push(rule.clone()); if rule.selector.compounds.len() == 1 { if let Some(compound) = rule.selector.compounds.first() { let tag_key = compound.tag.as_deref().unwrap_or("*"); index.by_tag.entry(tag_key.to_string()).or_default().push(i); if tag_key == "*" { index.universal_rules.push(i); } for cls in &compound.classes { index.by_class.entry(cls.clone()).or_default().push(i); index.by_tag_class.entry((tag_key.to_string(), cls.clone())).or_default().push(i); } if let Some(id) = &compound.id { index.by_id.entry(id.clone()).or_default().push(i); index.by_tag_id.entry((tag_key.to_string(), id.clone())).or_default().push(i); } } } else { if let Some(compound) = rule.selector.compounds.last() { let tag_key = compound.tag.as_deref().unwrap_or("*"); index.by_tag.entry(tag_key.to_string()).or_default().push(i); if tag_key == "*" { index.universal_rules.push(i); } for cls in &compound.classes { index.by_class.entry(cls.clone()).or_default().push(i); index.by_tag_class.entry((tag_key.to_string(), cls.clone())).or_default().push(i); } if let Some(id) = &compound.id { index.by_id.entry(id.clone()).or_default().push(i); index.by_tag_id.entry((tag_key.to_string(), id.clone())).or_default().push(i); } } index.complex_rules.push((i, i)); } } self.index = Some(index); } pub fn has_index(&self) -> bool { self.index.is_some() } pub fn query_index<'a>( &'a self, type_name: &str, el_id: Option<&str>, el_classes: &[&str], active_pseudo: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], el_attributes: &HashMap, ) -> Option>> { let index = self.index.as_ref()?; let mut candidates: HashSet = HashSet::with_capacity(32); candidates.extend(&index.universal_rules); if let Some(rules) = index.by_tag.get(type_name) { candidates.extend(rules); } for cls in el_classes { if let Some(rules) = index.by_class.get(*cls) { candidates.extend(rules); } } if let Some(id) = el_id { if let Some(rules) = index.by_id.get(id) { candidates.extend(rules); } } for (_, rule_id) in &index.complex_rules { candidates.insert(*rule_id); } let mut matched: Vec<(RuleId, &StyleRule)> = candidates .iter() .filter(|&&rule_id| { let rule = &index.rules[rule_id]; rule.selector .matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) }) .map(|&rule_id| (rule_id, &index.rules[rule_id])) .collect(); matched.sort_by(|(i, _a), (j, _b)| { index.rule_specificities[*i] .cmp(&index.rule_specificities[*j]) .then_with(|| i.cmp(j)) }); Some(matched.into_iter().map(|(_, rule)| &rule.properties).collect()) } pub fn matching_rules<'a>( &'a self, type_name: &str, el_id: Option<&str>, el_classes: &[&str], active_pseudo: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], el_attributes: &HashMap, ) -> Vec<&'a HashMap> { if let Some(matched) = self.query_index(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) { return matched; } let mut matched: Vec<(usize, &StyleRule)> = self .rules .iter() .enumerate() .filter(|(_, rule)| { rule.selector .matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes) }) .collect(); matched.sort_by(|(i, a), (j, b)| { a.selector .specificity() .cmp(&b.selector.specificity()) .then_with(|| i.cmp(j)) }); matched.into_iter().map(|(_, rule)| &rule.properties).collect() } pub fn query_index_for_pseudo( &self, pseudo: &str, type_name: &str, el_id: Option<&str>, el_classes: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], el_attributes: &HashMap, ) -> Option> { let index = self.index.as_ref()?; let mut candidates: HashSet = HashSet::with_capacity(16); candidates.extend(&index.universal_rules); if let Some(rules) = index.by_tag.get(type_name) { candidates.extend(rules); } for cls in el_classes { if let Some(rules) = index.by_class.get(*cls) { candidates.extend(rules); } } if let Some(id) = el_id { if let Some(rules) = index.by_id.get(id) { candidates.extend(rules); } } for (_, rule_id) in &index.complex_rules { candidates.insert(*rule_id); } let mut props = HashMap::new(); for &rule_id in &candidates { let rule = &index.rules[rule_id]; if !rule.selector.has_pseudo_class(pseudo) { continue; } if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) { continue; } for (k, v) in &rule.properties { props.insert(k.clone(), v.clone()); } } Some(props) } #[cfg(feature = "parallel")] pub fn matching_rules_batch<'a>( &'a self, type_names: &[&str], el_ids: &[Option<&str>], el_classes_list: &[&[&str]], active_pseudo_list: &[&[&str]], structural_list: &[&StructuralContext], ancestors_list: &[&[AncestorInfo]], preceding_siblings_list: &[&[AncestorInfo]], el_attributes_list: &[&HashMap], ) -> Vec>> { use rayon::prelude::*; (0..type_names.len()) .into_par_iter() .map(|i| { self.matching_rules( type_names[i], el_ids[i], el_classes_list[i], active_pseudo_list[i], structural_list[i], ancestors_list[i], preceding_siblings_list[i], el_attributes_list[i], ) }) .collect() } pub fn matching_pseudo_rules( &self, pseudo: &str, type_name: &str, el_id: Option<&str>, el_classes: &[&str], structural: &StructuralContext, ancestors: &[AncestorInfo], preceding_siblings: &[AncestorInfo], el_attributes: &HashMap, ) -> HashMap { if let Some(props) = self.query_index_for_pseudo(pseudo, type_name, el_id, el_classes, structural, ancestors, preceding_siblings, el_attributes) { return props; } let mut props = HashMap::new(); for rule in &self.rules { if !rule.selector.has_pseudo_class(pseudo) { continue; } if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) { continue; } for (k, v) in &rule.properties { props.insert(k.clone(), v.clone()); } } props } pub fn compute_cached<'a>( &self, type_name: &str, props: &[(Cow<'_, str>, Cow<'_, str>)], matched_sheets: &[&'a HashMap], ) -> ComputedStyle { let _scope = crate::perf::PerfScope::new("style"); let epoch = self.epoch; self.cache.lock().unwrap().get_or_compute(type_name, props, epoch, matched_sheets) } pub fn clear_cache(&self) { self.cache.lock().unwrap().clear(); } pub fn is_empty(&self) -> bool { self.rules.is_empty() } } fn split_selectors(input: &str) -> Vec { let mut parts = Vec::new(); let mut depth = 0u32; let mut start = 0usize; for (i, ch) in input.char_indices() { match ch { '(' | '[' => depth += 1, ')' | ']' => depth = depth.saturating_sub(1), ',' if depth == 0 => { let part = input[start..i].trim(); if !part.is_empty() { parts.push(part.to_string()); } start = i + 1; } _ => {} } } let last = input[start..].trim(); if !last.is_empty() { parts.push(last.to_string()); } parts } pub type RuleId = usize; #[derive(Debug, Clone)] pub struct StyleIndex { pub by_tag: HashMap>, pub by_class: HashMap>, pub by_id: HashMap>, pub by_tag_class: HashMap<(String, String), Vec>, pub by_tag_id: HashMap<(String, String), Vec>, pub complex_rules: Vec<(RuleId, RuleId)>, pub universal_rules: Vec, pub rule_specificities: Vec<(u32, u32, u32)>, pub rules: Vec, pub epoch: u64, } impl StyleIndex { pub fn new() -> Self { Self { by_tag: HashMap::new(), by_class: HashMap::new(), by_id: HashMap::new(), by_tag_class: HashMap::new(), by_tag_id: HashMap::new(), complex_rules: Vec::new(), universal_rules: Vec::new(), rule_specificities: Vec::new(), rules: Vec::new(), epoch: 0, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Overflow { #[default] Visible, Hidden, Scroll, Auto, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Position { #[default] Static, Relative, Absolute, Sticky, Fixed, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Display { #[default] Block, Flex, Grid, Inline, None, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum SizeValue { Px(f32), Percent(f32), } impl SizeValue { pub fn resolve(self, relative_to: Option) -> f32 { match self { SizeValue::Px(v) => v, SizeValue::Percent(p) => relative_to.map(|base| base * p / 100.0).unwrap_or(p), } } } #[derive(Debug, Clone, Copy, Default)] pub struct ComputedStyle { pub font_size: Option, pub color: Option, pub padding: Option, pub padding_top: Option, pub padding_right: Option, pub padding_bottom:Option, pub padding_left: Option, pub margin: Option, pub margin_top: Option, pub margin_right: Option, pub margin_bottom: Option, pub margin_left: Option, pub background: Option, pub spacing: Option, pub border_radius: Option, pub border_width: Option, pub border_color: Option, pub width: Option, pub height: Option, pub min_width: Option, pub max_width: Option, pub min_height: Option, pub max_height: Option, pub direction: Option, pub align_items: Option, pub content_align: Option, pub flex_grow: Option, pub position: Option, pub top: Option, pub right: Option, pub bottom: Option, pub left: Option, pub overflow_x: Option, pub overflow_y: Option, pub display: Option, pub opacity: Option, pub font_weight: Option, pub line_height: Option, pub text_align: Option, } impl ComputedStyle { pub fn compute( inline: &[(Cow<'_, str>, Cow<'_, str>)], matched_sheets: &[&HashMap], ) -> Self { let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow); Self { font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size), color: lookup("color", inline, matched_sheets).and_then(parse_color), padding: lookup("padding", inline, matched_sheets).and_then(parse_size), padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size), padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size), padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size), padding_left: lookup("padding-left", inline, matched_sheets).and_then(parse_size), margin: lookup("margin", inline, matched_sheets).and_then(parse_size), margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size), margin_right: lookup("margin-right", inline, matched_sheets).and_then(parse_size), margin_bottom: lookup("margin-bottom", inline, matched_sheets).and_then(parse_size), margin_left: lookup("margin-left", inline, matched_sheets).and_then(parse_size), background: lookup("background", inline, matched_sheets) .or_else(|| lookup("background-color", inline, matched_sheets)) .and_then(parse_color), spacing: lookup("spacing", inline, matched_sheets) .or_else(|| lookup("gap", inline, matched_sheets)) .and_then(parse_size), border_radius: lookup("border-radius", inline, matched_sheets).and_then(parse_size), border_width: lookup("border-width", inline, matched_sheets).and_then(parse_size), border_color: lookup("border-color", inline, matched_sheets).and_then(parse_color), width: lookup("width", inline, matched_sheets).and_then(parse_length), height: lookup("height", inline, matched_sheets).and_then(parse_length), min_width: lookup("min-width", inline, matched_sheets).and_then(parse_size), max_width: lookup("max-width", inline, matched_sheets).and_then(parse_size), min_height: lookup("min-height", inline, matched_sheets).and_then(parse_size), max_height: lookup("max-height", inline, matched_sheets).and_then(parse_size), direction: lookup("direction", inline, matched_sheets).and_then(parse_direction), align_items: lookup("align-items", inline, matched_sheets).and_then(parse_alignment), content_align: lookup("content-align", inline, matched_sheets).and_then(parse_content_align), flex_grow: lookup("flex-grow", inline, matched_sheets) .and_then(|s| s.trim().parse::().ok()) .map(|v| v as u16), position: lookup("position", inline, matched_sheets).and_then(parse_position), top: lookup("top", inline, matched_sheets).and_then(parse_size), right: lookup("right", inline, matched_sheets).and_then(parse_size), bottom: lookup("bottom", inline, matched_sheets).and_then(parse_size), left: lookup("left", inline, matched_sheets).and_then(parse_size), overflow_x: lookup("overflow-x", inline, matched_sheets) .and_then(parse_overflow) .or(overflow), overflow_y: lookup("overflow-y", inline, matched_sheets) .and_then(parse_overflow) .or(overflow), display: lookup("display", inline, matched_sheets).and_then(parse_display), opacity: lookup("opacity", inline, matched_sheets).and_then(parse_opacity), font_weight: lookup("font-weight", inline, matched_sheets).and_then(parse_font_weight), line_height: lookup("line-height", inline, matched_sheets).and_then(parse_size), text_align: lookup("text-align", inline, matched_sheets).and_then(parse_text_align), } } #[cfg(feature = "parallel")] pub fn compute_batch<'a>( pairs: &[(&[(Cow<'_, str>, Cow<'_, str>)], &[&'a HashMap])], ) -> Vec { use rayon::prelude::*; pairs.par_iter().map(|(inline, sheets)| Self::compute(inline, sheets)).collect() } pub fn apply_overrides(&mut self, sheet: &HashMap) { struct Override<'a>(&'a HashMap); impl<'a> Override<'a> { fn get(&self, key: &str) -> Option<&'a str> { self.0.get(key).map(|s| s.as_str()) } } let ov = Override(sheet); if let Some(v) = ov.get("font-size") { self.font_size = parse_size(v); } if let Some(v) = ov.get("color") { self.color = parse_color(v); } if let Some(v) = ov.get("padding") { self.padding = parse_size(v); } if let Some(v) = ov.get("padding-top") { self.padding_top = parse_size(v); } if let Some(v) = ov.get("padding-right") { self.padding_right = parse_size(v); } if let Some(v) = ov.get("padding-bottom") { self.padding_bottom = parse_size(v); } if let Some(v) = ov.get("padding-left") { self.padding_left = parse_size(v); } if let Some(v) = ov.get("margin") { self.margin = parse_size(v); } if let Some(v) = ov.get("margin-top") { self.margin_top = parse_size(v); } if let Some(v) = ov.get("margin-right") { self.margin_right = parse_size(v); } if let Some(v) = ov.get("margin-bottom") { self.margin_bottom = parse_size(v); } if let Some(v) = ov.get("margin-left") { self.margin_left = parse_size(v); } if let Some(v) = ov.get("background").or_else(|| ov.get("background-color")) { self.background = parse_color(v); } if let Some(v) = ov.get("spacing").or_else(|| ov.get("gap")) { self.spacing = parse_size(v); } if let Some(v) = ov.get("border-radius") { self.border_radius = parse_size(v); } if let Some(v) = ov.get("border-width") { self.border_width = parse_size(v); } if let Some(v) = ov.get("border-color") { self.border_color = parse_color(v); } if let Some(v) = ov.get("width") { self.width = parse_length(v); } if let Some(v) = ov.get("height") { self.height = parse_length(v); } if let Some(v) = ov.get("direction") { self.direction = parse_direction(v); } if let Some(v) = ov.get("align-items") { self.align_items = parse_alignment(v); } if let Some(v) = ov.get("content-align") { self.content_align = parse_content_align(v); } if let Some(v) = ov.get("flex-grow") { self.flex_grow = v.trim().parse::().ok().map(|x| x as u16); } if let Some(v) = ov.get("position") { self.position = parse_position(v); } if let Some(v) = ov.get("top") { self.top = parse_size(v); } if let Some(v) = ov.get("right") { self.right = parse_size(v); } if let Some(v) = ov.get("bottom") { self.bottom = parse_size(v); } if let Some(v) = ov.get("left") { self.left = parse_size(v); } if let Some(v) = ov.get("overflow") { let o = parse_overflow(v); if o.is_some() { self.overflow_x = o; self.overflow_y = o; } } if let Some(v) = ov.get("overflow-x") { self.overflow_x = parse_overflow(v); } if let Some(v) = ov.get("overflow-y") { self.overflow_y = parse_overflow(v); } if let Some(v) = ov.get("display") { self.display = parse_display(v); } if let Some(v) = ov.get("opacity") { self.opacity = parse_opacity(v); } if let Some(v) = ov.get("font-weight") { self.font_weight = parse_font_weight(v); } if let Some(v) = ov.get("line-height") { self.line_height = parse_size(v); } if let Some(v) = ov.get("text-align") { self.text_align = parse_text_align(v); } } } #[inline] fn lookup<'a>( key: &str, inline: &'a [(Cow<'_, str>, Cow<'_, str>)], matched_sheets: &[&'a HashMap], ) -> Option<&'a str> { for (k, v) in inline { if **k == *key { return Some(v.as_ref()); } if k.len() == key.len() + 6 && k.starts_with("style:") && &k[6..] == key { return Some(v.as_ref()); } } for sheet in matched_sheets.iter().rev() { if let Some(v) = sheet.get(key) { return Some(v.as_str()); } } None } pub fn parse_overflow(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("visible") { Some(Overflow::Visible) } else if s.eq_ignore_ascii_case("hidden") { Some(Overflow::Hidden) } else if s.eq_ignore_ascii_case("scroll") { Some(Overflow::Scroll) } else if s.eq_ignore_ascii_case("auto") { Some(Overflow::Auto) } else { None } } pub fn parse_position(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("fixed") { Some(Position::Fixed) } else if s.eq_ignore_ascii_case("sticky") { Some(Position::Sticky) } else if s.eq_ignore_ascii_case("absolute") { Some(Position::Absolute) } else if s.eq_ignore_ascii_case("relative") { Some(Position::Relative) } else if s.eq_ignore_ascii_case("static") { Some(Position::Static) } else { None } } pub fn parse_length(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("100%") || s.eq_ignore_ascii_case("100vw") || s.eq_ignore_ascii_case("100vh") || s.eq_ignore_ascii_case("stretch") { return Some(iced::Length::Fill); } if s.eq_ignore_ascii_case("shrink") || s.eq_ignore_ascii_case("fit-content") || s.eq_ignore_ascii_case("auto") { return Some(iced::Length::Shrink); } let val = s.trim_end_matches(|c: char| c.is_alphabetic() || c == '%').parse::().ok()?; if s.ends_with('%') { if val >= 100.0 { Some(iced::Length::Fill) } else { Some(iced::Length::FillPortion(val as u16)) } } else { Some(iced::Length::Fixed(val)) } } pub fn parse_color(s: &str) -> Option { let s = s.trim(); if let Some(hex) = s.strip_prefix('#') { return match hex.len() { 3 => { let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?; let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?; let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?; Some(iced::Color::from_rgb( r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, )) } 6 => { let r = u8::from_str_radix(&hex[0..2], 16).ok()?; let g = u8::from_str_radix(&hex[2..4], 16).ok()?; let b = u8::from_str_radix(&hex[4..6], 16).ok()?; Some(iced::Color::from_rgb( r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, )) } 8 => { let r = u8::from_str_radix(&hex[0..2], 16).ok()?; let g = u8::from_str_radix(&hex[2..4], 16).ok()?; let b = u8::from_str_radix(&hex[4..6], 16).ok()?; let a = u8::from_str_radix(&hex[6..8], 16).ok()?; Some(iced::Color::from_rgba( r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0, )) } _ => None, }; } match s { "white" => Some(iced::Color::WHITE), "black" => Some(iced::Color::BLACK), "transparent" => Some(iced::Color::TRANSPARENT), _ => None, } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LayoutDirection { Column, Row, Grid, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContentAlign { Start, Center, End, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextAlign { Left, Center, Right, } impl From for iced::alignment::Horizontal { fn from(ta: TextAlign) -> Self { match ta { TextAlign::Left => iced::alignment::Horizontal::Left, TextAlign::Center => iced::alignment::Horizontal::Center, TextAlign::Right => iced::alignment::Horizontal::Right, } } } pub fn parse_direction(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("horizontal") || s.eq_ignore_ascii_case("row") { Some(LayoutDirection::Row) } else if s.eq_ignore_ascii_case("vertical") || s.eq_ignore_ascii_case("column") { Some(LayoutDirection::Column) } else if s.eq_ignore_ascii_case("grid") { Some(LayoutDirection::Grid) } else { None } } pub fn parse_alignment(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) } else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) } else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) } else { None } } pub fn parse_content_align(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) } else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) } else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) } else { None } } pub fn parse_display(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("none") { Some(Display::None) } else if s.eq_ignore_ascii_case("block") { Some(Display::Block) } else if s.eq_ignore_ascii_case("flex") { Some(Display::Flex) } else if s.eq_ignore_ascii_case("grid") { Some(Display::Grid) } else if s.eq_ignore_ascii_case("inline") { Some(Display::Inline) } else { None } } pub fn parse_opacity(s: &str) -> Option { let v = s.trim().parse::().ok()?; Some(v.clamp(0.0, 1.0)) } pub fn parse_font_weight(s: &str) -> Option { let s = s.trim(); match s { "normal" => Some(400), "bold" => Some(700), "lighter" => Some(300), "bolder" => Some(900), _ => s.parse::().ok().map(|v| v as u16), } } pub fn parse_text_align(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("left") { Some(TextAlign::Left) } else if s.eq_ignore_ascii_case("center") { Some(TextAlign::Center) } else if s.eq_ignore_ascii_case("right") { Some(TextAlign::Right) } else { None } } pub fn parse_size(s: &str) -> Option { let s = s.trim(); if s.eq_ignore_ascii_case("auto") || s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") { return None; } if s.ends_with('%') { let val = s.trim_end_matches(|c: char| c == '%' || c.is_alphabetic()) .parse::().ok()?; return Some(SizeValue::Percent(val)); } if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") { return None; } let val = s.trim_end_matches(|c: char| c.is_alphabetic()) .parse::().ok()?; Some(SizeValue::Px(val)) } pub fn resolve_size(v: Option, relative_to: Option) -> Option { v.map(|sv| sv.resolve(relative_to)) } #[cfg(test)] mod tests { use super::*; fn make_style_sheet(rules: &[(&str, &[(&str, &str)])]) -> StyleSheet { let mut ss = StyleSheet::new(); for (selector, props) in rules { let mut map = HashMap::new(); for (k, v) in *props { map.insert(k.to_string(), v.to_string()); } ss.add_rule(selector.to_string(), map); } ss.build_index(); ss } #[test] fn test_style_index_basic() { let ss = make_style_sheet(&[ ("Button", &[("color", "red")]), ("Button.primary", &[("color", "blue")]), ("#submit", &[("font-size", "20")]), ("Label", &[("color", "green")]), ]); let attrs = HashMap::new(); let structural = StructuralContext::default(); // Button class="" should match "Button" rule let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 1, "Button should match 1 rule"); assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("red")); // Button class="primary" should match both "Button" and "Button.primary" let matched = ss.matching_rules("Button", None, &["primary"], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 2, "Button.primary should match 2 rules"); // The more specific rule comes first (or last depending on order) let colors: Vec<&str> = matched.iter().filter_map(|m| m.get("color").map(|s| s.as_str())).collect(); assert!(colors.contains(&"red")); assert!(colors.contains(&"blue")); // Button id="submit" should match "Button" and "#submit" let matched = ss.matching_rules("Button", Some("submit"), &[], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 2, "Button#submit should match 2 rules"); assert!(matched.iter().any(|m| m.contains_key("color"))); assert!(matched.iter().any(|m| m.contains_key("font-size"))); // Label should match "Label" let matched = ss.matching_rules("Label", None, &[], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 1); assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("green")); } #[test] fn test_style_index_empty() { let ss = make_style_sheet(&[]); let attrs = HashMap::new(); let structural = StructuralContext::default(); let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); assert!(matched.is_empty()); } #[test] fn test_style_index_pseudo() { let ss = make_style_sheet(&[ ("Button:hover", &[("color", "red")]), ("Button", &[("color", "blue")]), ]); let attrs = HashMap::new(); let structural = StructuralContext::default(); // No pseudo let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 1); assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("blue")); // With hover pseudo let matched = ss.matching_rules("Button", None, &[], &["hover"], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 2, "Button:hover should match 2 rules with pseudo"); } #[test] fn test_style_index_complex() { let ss = make_style_sheet(&[ ("Panel > Button", &[("color", "red")]), ("Panel Button", &[("font-size", "16")]), ]); let attrs = HashMap::new(); let structural = StructuralContext::default(); // Button with Panel ancestor let ancestors = [AncestorInfo::new("Panel", vec![])]; let matched = ss.matching_rules("Button", None, &[], &[], &structural, &ancestors, &[], &attrs); assert_eq!(matched.len(), 2, "Both complex rules should match"); // Button without Panel ancestor let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); assert_eq!(matched.len(), 0, "No matching without Panel ancestor"); } #[test] fn test_style_index_off_by_one_same_as_fallback() { // Verify index gives same results as fallback for various inputs let ss = make_style_sheet(&[ ("*", &[("margin", "0")]), ("Button", &[("padding", "10")]), ("Button.primary#submit", &[("color", "red")]), ("Label:hover", &[("color", "blue")]), (".highlight", &[("background", "yellow")]), ]); let attrs: HashMap = HashMap::new(); let structural = StructuralContext::default(); // Verify index matches expected behavior let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs); assert!(!matched.is_empty(), "Button should match universal rule"); assert!(matched.iter().any(|m| m.get("margin").map(|s| s.as_str()) == Some("0")), "Should include universal margin"); assert!(matched.iter().any(|m| m.get("padding").map(|s| s.as_str()) == Some("10")), "Should include Button padding"); let matched = ss.matching_rules("Button", Some("submit"), &["primary"], &[], &structural, &[], &[], &attrs); assert!(matched.iter().any(|m| m.contains_key("color")), "#submit.primary should match color rule"); let matched = ss.matching_rules("Label", None, &["highlight"], &[], &structural, &[], &[], &attrs); assert!(matched.iter().any(|m| m.contains_key("background")), "highlight class should match"); let matched = ss.matching_rules("Panel", None, &[], &[], &structural, &[], &[], &attrs); assert!(matched.iter().any(|m| m.contains_key("margin")), "Panel should match universal rule"); } #[test] fn test_style_index_epoch_increases() { let mut ss = make_style_sheet(&[("Button", &[("color", "red")])]); let epoch1 = ss.index.as_ref().unwrap().epoch; ss.add_rule("Label".to_string(), { let mut m = HashMap::new(); m.insert("color".to_string(), "blue".to_string()); m }); assert!(ss.index.is_none(), "add_rule should invalidate index"); ss.build_index(); let epoch2 = ss.index.as_ref().unwrap().epoch; assert!(epoch2 > epoch1, "build_index should increase epoch"); } }