10.1: Add Criterion dev-dependency and benchmarks for:
- matching_rules (100 rules)
- ComputedStyle::compute (empty, 5 props)
- resolve_string (no vars, 1 var, 3 vars)
10.3: Add --perf CLI flag and src/perf.rs module:
- PerfScope drop-guard timed scopes (vdom, style, render)
- Instrumented: VDOM eval (app.rs update), render (app.rs view),
style matching (style.rs compute_cached)
- Per-frame report printed to stderr at end of view()
10.2+10.4: Threshold check in perf report:
- Prints warning if total > 16ms (60 FPS frame budget)
Refactor: Move Message enum to lib.rs so benchmarks can import
the public API. Added lib.rs as library root.
1505 lines
52 KiB
Rust
1505 lines
52 KiB
Rust
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<String>,
|
|
pub id: Option<String>,
|
|
pub classes: Vec<String>,
|
|
pub pseudo_classes: Vec<String>,
|
|
pub attributes: Vec<AttributeSelector>,
|
|
}
|
|
|
|
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<char> = 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<String, String>,
|
|
) -> 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<CompoundSelector>,
|
|
pub combinators: Vec<Combinator>,
|
|
}
|
|
|
|
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<CompoundSelector> = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect();
|
|
let combinators: Vec<Combinator> = 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<String, String>,
|
|
) -> 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<String>,
|
|
pub classes: Vec<String>,
|
|
}
|
|
|
|
impl AncestorInfo {
|
|
pub fn new(type_name: &str, classes: Vec<String>) -> Self {
|
|
Self { type_name: type_name.to_string(), id: None, classes }
|
|
}
|
|
|
|
pub fn new_with_id(type_name: &str, id: Option<String>, classes: Vec<String>) -> 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::<i32>() {
|
|
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::<i32>().unwrap_or(0)
|
|
};
|
|
|
|
let b = if b_str.is_empty() {
|
|
0
|
|
} else if b_str.starts_with('+') {
|
|
b_str[1..].trim().parse::<i32>().unwrap_or(0)
|
|
} else if b_str.starts_with('-') {
|
|
b_str.parse::<i32>().unwrap_or(0)
|
|
} else {
|
|
b_str.parse::<i32>().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<String, String>,
|
|
}
|
|
|
|
impl StyleRule {
|
|
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self {
|
|
Self {
|
|
selector: ComplexSelector::parse(&selector_str),
|
|
properties,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct StyleCache {
|
|
entries: HashMap<u64, ComputedStyle>,
|
|
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<String, String>],
|
|
) -> 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<StyleRule>,
|
|
index: Option<StyleIndex>,
|
|
epoch: u64,
|
|
cache: Mutex<StyleCache>,
|
|
}
|
|
|
|
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<String, String>) {
|
|
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<String, String>,
|
|
) -> Option<Vec<&'a HashMap<String, String>>> {
|
|
let index = self.index.as_ref()?;
|
|
|
|
let mut candidates: HashSet<RuleId> = 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<String, String>,
|
|
) -> Vec<&'a HashMap<String, String>> {
|
|
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<String, String>,
|
|
) -> Option<HashMap<String, String>> {
|
|
let index = self.index.as_ref()?;
|
|
|
|
let mut candidates: HashSet<RuleId> = 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<String, String>],
|
|
) -> Vec<Vec<&'a HashMap<String, String>>> {
|
|
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<String, String>,
|
|
) -> HashMap<String, String> {
|
|
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<String, String>],
|
|
) -> 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<String> {
|
|
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<String, Vec<RuleId>>,
|
|
pub by_class: HashMap<String, Vec<RuleId>>,
|
|
pub by_id: HashMap<String, Vec<RuleId>>,
|
|
pub by_tag_class: HashMap<(String, String), Vec<RuleId>>,
|
|
pub by_tag_id: HashMap<(String, String), Vec<RuleId>>,
|
|
pub complex_rules: Vec<(RuleId, RuleId)>,
|
|
pub universal_rules: Vec<RuleId>,
|
|
pub rule_specificities: Vec<(u32, u32, u32)>,
|
|
pub rules: Vec<StyleRule>,
|
|
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>) -> 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<SizeValue>,
|
|
pub color: Option<iced::Color>,
|
|
pub padding: Option<SizeValue>,
|
|
pub padding_top: Option<SizeValue>,
|
|
pub padding_right: Option<SizeValue>,
|
|
pub padding_bottom:Option<SizeValue>,
|
|
pub padding_left: Option<SizeValue>,
|
|
|
|
pub margin: Option<SizeValue>,
|
|
pub margin_top: Option<SizeValue>,
|
|
pub margin_right: Option<SizeValue>,
|
|
pub margin_bottom: Option<SizeValue>,
|
|
pub margin_left: Option<SizeValue>,
|
|
|
|
pub background: Option<iced::Color>,
|
|
pub spacing: Option<SizeValue>,
|
|
pub border_radius: Option<SizeValue>,
|
|
pub border_width: Option<SizeValue>,
|
|
pub border_color: Option<iced::Color>,
|
|
|
|
pub width: Option<iced::Length>,
|
|
pub height: Option<iced::Length>,
|
|
|
|
pub min_width: Option<SizeValue>,
|
|
pub max_width: Option<SizeValue>,
|
|
pub min_height: Option<SizeValue>,
|
|
pub max_height: Option<SizeValue>,
|
|
pub direction: Option<LayoutDirection>,
|
|
pub align_items: Option<iced::Alignment>,
|
|
pub content_align: Option<ContentAlign>,
|
|
|
|
pub flex_grow: Option<u16>,
|
|
|
|
pub position: Option<Position>,
|
|
pub top: Option<SizeValue>,
|
|
pub right: Option<SizeValue>,
|
|
pub bottom: Option<SizeValue>,
|
|
pub left: Option<SizeValue>,
|
|
|
|
pub overflow_x: Option<Overflow>,
|
|
pub overflow_y: Option<Overflow>,
|
|
pub display: Option<Display>,
|
|
|
|
pub opacity: Option<f32>,
|
|
pub font_weight: Option<u16>,
|
|
pub line_height: Option<SizeValue>,
|
|
pub text_align: Option<TextAlign>,
|
|
}
|
|
|
|
impl ComputedStyle {
|
|
pub fn compute(
|
|
inline: &[(Cow<'_, str>, Cow<'_, str>)],
|
|
matched_sheets: &[&HashMap<String, String>],
|
|
) -> 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::<f32>().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<String, String>])],
|
|
) -> Vec<ComputedStyle> {
|
|
use rayon::prelude::*;
|
|
pairs.par_iter().map(|(inline, sheets)| Self::compute(inline, sheets)).collect()
|
|
}
|
|
|
|
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
|
|
struct Override<'a>(&'a HashMap<String, String>);
|
|
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::<f32>().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<String, String>],
|
|
) -> 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<Overflow> {
|
|
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<Position> {
|
|
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<iced::Length> {
|
|
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::<f32>().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<iced::Color> {
|
|
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<TextAlign> 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<LayoutDirection> {
|
|
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<iced::Alignment> {
|
|
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<ContentAlign> {
|
|
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<Display> {
|
|
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<f32> {
|
|
let v = s.trim().parse::<f32>().ok()?;
|
|
Some(v.clamp(0.0, 1.0))
|
|
}
|
|
|
|
pub fn parse_font_weight(s: &str) -> Option<u16> {
|
|
let s = s.trim();
|
|
match s {
|
|
"normal" => Some(400),
|
|
"bold" => Some(700),
|
|
"lighter" => Some(300),
|
|
"bolder" => Some(900),
|
|
_ => s.parse::<f32>().ok().map(|v| v as u16),
|
|
}
|
|
}
|
|
|
|
pub fn parse_text_align(s: &str) -> Option<TextAlign> {
|
|
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<SizeValue> {
|
|
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::<f32>().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::<f32>().ok()?;
|
|
Some(SizeValue::Px(val))
|
|
}
|
|
|
|
pub fn resolve_size(v: Option<SizeValue>, relative_to: Option<f32>) -> Option<f32> {
|
|
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<String, String> = 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");
|
|
}
|
|
}
|