use std::collections::HashMap;
use rdom_core::selectors::{ComplexSelector, PseudoClass, SimpleSelector};
use rdom_core::{NodeId, NodeType};
use super::states::{RadioGroups, compute_in};
use crate::TuiDom;
use crate::style::{DirtyTracker, Stylesheet};
#[derive(Debug, Default)]
pub(crate) struct ValidityMarks {
last: HashMap<NodeId, bool>,
scratch: HashMap<NodeId, bool>,
primed: bool,
uses_validity: Option<bool>,
radio_groups: RadioGroups,
}
impl ValidityMarks {
pub(crate) fn sheets_changed(&mut self) {
self.uses_validity = None;
}
pub(crate) fn flush<'s>(
&mut self,
dom: &mut TuiDom,
tracker: &DirtyTracker,
sheets: impl IntoIterator<Item = &'s Stylesheet>,
) -> bool {
let uses = *self
.uses_validity
.get_or_insert_with(|| sheets.into_iter().any(uses_validity));
if !uses {
self.last.clear();
self.primed = false;
return false;
}
let mut now = std::mem::take(&mut self.scratch);
current(dom, &mut now, &mut self.radio_groups);
if self.primed {
let changed: Vec<NodeId> = now
.iter()
.filter(|(id, v)| self.last.get(id) != Some(v))
.map(|(&id, _)| id)
.collect();
for id in changed {
tracker.mark_dirty(dom, id);
}
}
self.scratch = std::mem::replace(&mut self.last, now);
self.scratch.clear();
self.primed = true;
true
}
}
fn current(dom: &TuiDom, out: &mut HashMap<NodeId, bool>, groups: &mut RadioGroups) {
out.clear();
groups.clear();
let mut invalid = Vec::new();
let mut stack = vec![dom.root()];
while let Some(id) = stack.pop() {
let node = dom.node(id);
stack.extend(node.child_nodes().map(|c| c.id()));
if node.node_type() != NodeType::Element {
continue;
}
match node.tag_name() {
Some("form" | "fieldset") => {
out.insert(id, true);
}
_ if dom.will_validate(id) => {
let valid = compute_in(dom, id, groups).valid();
out.insert(id, valid);
if !valid {
invalid.push(id);
}
}
_ => {}
}
}
for c in invalid {
if let Some(form) = dom.form_owner(c)
&& let Some(v) = out.get_mut(&form)
{
*v = false;
}
let mut up = dom.node(c).parent_node().map(|p| p.id());
while let Some(a) = up {
let node = dom.node(a);
if node.tag_name() == Some("fieldset")
&& let Some(v) = out.get_mut(&a)
{
*v = false;
}
up = node.parent_node().map(|p| p.id());
}
}
}
pub(super) fn uses_validity(sheet: &Stylesheet) -> bool {
sheet
.rules()
.iter()
.any(|r| r.selector.0.iter().any(complex_uses_validity))
}
fn complex_uses_validity(c: &ComplexSelector) -> bool {
std::iter::once(&c.subject)
.chain(c.ancestors.iter().map(|(_, compound)| compound))
.flat_map(|compound| &compound.simples)
.any(|s| match s {
SimpleSelector::Pseudo(p) => matches!(p, PseudoClass::Valid | PseudoClass::Invalid),
SimpleSelector::Not(list) | SimpleSelector::Where(list) => {
list.0.iter().any(complex_uses_validity)
}
_ => false,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn el(dom: &mut TuiDom, parent: NodeId, tag: &str, attrs: &[(&str, &str)]) -> NodeId {
let e = dom.create_element(tag);
for (k, v) in attrs {
dom.set_attribute(e, k, v).unwrap();
}
dom.append_child(parent, e).unwrap();
crate::runtime::builtins::input::ensure_seeded(dom, e);
e
}
#[test]
fn one_walk_verdicts_match_constraint_validity() {
let mut dom: TuiDom = TuiDom::new();
super::super::install(&mut dom);
let root = dom.root();
let f1 = el(&mut dom, root, "form", &[("id", "f1")]);
let f2 = el(&mut dom, root, "form", &[("id", "f2")]);
let outer = el(&mut dom, f1, "fieldset", &[]);
let inner = el(&mut dom, outer, "fieldset", &[]);
el(&mut dom, inner, "input", &[("required", "")]);
el(
&mut dom,
inner,
"input",
&[("required", ""), ("form", "f2")],
);
el(&mut dom, outer, "input", &[("value", "ok")]);
let quiet = el(&mut dom, f1, "fieldset", &[]);
el(
&mut dom,
quiet,
"input",
&[("required", ""), ("value", "x")],
);
el(
&mut dom,
quiet,
"input",
&[("required", ""), ("disabled", "")],
);
for form in ["f1", "f2"] {
el(
&mut dom,
root,
"input",
&[("type", "radio"), ("name", "g"), ("form", form)],
);
}
el(
&mut dom,
f1,
"input",
&[("type", "radio"), ("name", "g"), ("required", "")],
);
el(
&mut dom,
f2,
"input",
&[("type", "radio"), ("name", "g"), ("checked", "")],
);
el(
&mut dom,
root,
"input",
&[("type", "email"), ("value", "nope")],
);
el(&mut dom, root, "select", &[("required", "")]);
el(&mut dom, root, "textarea", &[]);
let mut out = HashMap::new();
current(&dom, &mut out, &mut RadioGroups::default());
let mut all = Vec::new();
let mut stack = vec![root];
while let Some(id) = stack.pop() {
stack.extend(dom.node(id).child_nodes().map(|c| c.id()));
all.push(id);
}
let mut checked = 0;
for id in all {
assert_eq!(out.get(&id).copied(), dom.constraint_validity(id), "{id:?}");
checked += usize::from(out.contains_key(&id));
}
assert!(
checked > 10,
"the fixture exercises the verdicts: {checked}"
);
assert_eq!(out.get(&f1), Some(&false));
assert_eq!(out.get(&f2), Some(&false));
assert_eq!(out.get(&quiet), Some(&true));
assert_eq!(out.get(&inner), Some(&false));
}
}