use std::cell::Cell;
use std::rc::Rc;
use rdom_core::{Dom, Mutation, MutationObserver, NodeId};
use rdom_css::Warning;
use rdom_style::Stylesheet;
use crate::{TuiDom, TuiExt};
#[derive(Debug)]
struct StyleSheetEntry {
element: NodeId,
source: String,
sheet: Stylesheet,
warnings: Vec<Warning>,
}
#[derive(Debug)]
pub(crate) struct StyleElements {
dirty: Rc<Cell<bool>>,
entries: Vec<StyleSheetEntry>,
}
impl StyleElements {
pub(crate) fn install(dom: &mut TuiDom) -> Self {
let dirty = Rc::new(Cell::new(true));
dom.add_mutation_observer(Box::new(StyleObserver {
dirty: dirty.clone(),
}));
Self {
dirty,
entries: Vec::new(),
}
}
pub(crate) fn flush(&mut self, dom: &TuiDom) -> bool {
if !self.dirty.replace(false) {
return false;
}
let mut previous = std::mem::take(&mut self.entries);
let before: Vec<NodeId> = previous.iter().map(|e| e.element).collect();
let mut reparsed = false;
let styles = dom.elements_by_tag("style");
for &element in styles.ids() {
let source = crate::node::child_text(dom, element);
let reused = previous
.iter()
.position(|e| e.element == element && e.source == source)
.map(|at| previous.swap_remove(at));
let entry = reused.unwrap_or_else(|| {
reparsed = true;
let parsed = rdom_css::parse(&source);
StyleSheetEntry {
element,
source,
sheet: parsed.stylesheet,
warnings: parsed.warnings,
}
});
self.entries.push(entry);
}
reparsed || !self.entries.iter().map(|e| e.element).eq(before)
}
pub(crate) fn sheets(&self) -> impl Iterator<Item = &Stylesheet> {
self.entries.iter().map(|e| &e.sheet)
}
pub(crate) fn warnings(&self) -> impl Iterator<Item = &Warning> {
self.entries.iter().flat_map(|e| &e.warnings)
}
}
struct StyleObserver {
dirty: Rc<Cell<bool>>,
}
impl MutationObserver<TuiExt> for StyleObserver {
fn observe(&mut self, dom: &mut Dom<TuiExt>, record: &Mutation) {
if self.dirty.get() {
return;
}
let relevant = match record {
Mutation::CharacterDataChanged { id, .. } => dom
.node(*id)
.parent_node()
.is_some_and(|p| p.tag_name() == Some("style") && p.is_connected()),
Mutation::ChildListChanged {
parent,
added,
removed,
} => {
dom.node(*parent).is_connected()
&& (dom.node(*parent).tag_name() == Some("style")
|| added
.iter()
.chain(removed)
.any(|&id| !dom.contains(id) || holds_style(dom, id)))
}
_ => false,
};
if relevant {
self.dirty.set(true);
}
}
}
fn holds_style(dom: &TuiDom, id: NodeId) -> bool {
let mut cur = id;
loop {
let node = dom.node(cur);
if node.tag_name() == Some("style") {
return true;
}
if let Some(child) = node.first_child() {
cur = child.id();
continue;
}
loop {
if cur == id {
return false;
}
let node = dom.node(cur);
if let Some(next) = node.next_sibling() {
cur = next.id();
break;
}
match node.parent_node() {
Some(parent) => cur = parent.id(),
None => return false,
}
}
}
}
#[cfg(test)]
mod tests;