use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::Position;
use crate::style::{ComputedStyle, PseudoElementTarget, Rule, Stylesheet, VarMap};
use super::apply::{apply_cascade_ladder, finalize_bfc_formation, finalize_border_fg};
use super::content::resolve_content_on;
use super::inherit::{inherit_inheritable_from, layout_differs};
pub(super) fn merge_root_vars(sheets: &[&Stylesheet]) -> VarMap {
let mut merged = std::collections::HashMap::new();
for sheet in sheets {
for (k, v) in sheet.vars() {
merged.insert(k.clone(), v.clone());
}
}
std::rc::Rc::new(merged)
}
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct SubtreeFlags {
pub has_positioned_pseudo: bool,
pub has_collapse: bool,
}
impl SubtreeFlags {
fn merge(&mut self, other: SubtreeFlags) {
self.has_positioned_pseudo |= other.has_positioned_pseudo;
self.has_collapse |= other.has_collapse;
}
}
pub(super) fn cascade_subtree(
dom: &mut Dom<TuiExt>,
sheets: &[&Stylesheet],
merged_vars: &VarMap,
id: NodeId,
parent_computed: &ComputedStyle,
) -> SubtreeFlags {
let child_ids: Vec<NodeId> = dom.node(id).child_nodes().map(|n| n.id()).collect();
let is_element = dom.node(id).node_type() == NodeType::Element;
if !is_element {
let mut flags = SubtreeFlags::default();
for child in child_ids {
flags.merge(cascade_subtree(
dom,
sheets,
merged_vars,
child,
parent_computed,
));
}
return flags;
}
let (
computed,
computed_before,
computed_after,
computed_backdrop,
computed_selection,
computed_scrollbar,
computed_scrollbar_thumb,
) = {
let computed = compute_element_style(dom, sheets, merged_vars, id, parent_computed);
let cb = compute_pseudo_style(dom, sheets, id, &computed, PseudoElementTarget::Before);
let ca = compute_pseudo_style(dom, sheets, id, &computed, PseudoElementTarget::After);
let cbd = compute_pseudo_style(dom, sheets, id, &computed, PseudoElementTarget::Backdrop);
let csel = compute_pseudo_style(dom, sheets, id, &computed, PseudoElementTarget::Selection);
let needs_scrollbar = !matches!(
computed.overflow_x,
crate::layout::Overflow::Visible | crate::layout::Overflow::Hidden
) || !matches!(
computed.overflow_y,
crate::layout::Overflow::Visible | crate::layout::Overflow::Hidden
);
let (csb, csbt) = if needs_scrollbar {
(
compute_pseudo_style(dom, sheets, id, &computed, PseudoElementTarget::Scrollbar),
compute_pseudo_style(
dom,
sheets,
id,
&computed,
PseudoElementTarget::ScrollbarThumb,
),
)
} else {
(None, None)
};
(computed, cb, ca, cbd, csel, csb, csbt)
};
let own_has_positioned_pseudo = computed_before
.as_ref()
.is_some_and(|c| c.position != Position::Static)
|| computed_after
.as_ref()
.is_some_and(|c| c.position != Position::Static);
let layout_changed = match dom.node(id).ext().and_then(|e| e.computed.as_ref()) {
Some(prev) => layout_differs(prev, &computed),
None => true,
};
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.computed = Some(computed.clone());
ext.computed_before = computed_before;
ext.computed_after = computed_after;
ext.computed_backdrop = computed_backdrop;
ext.computed_selection = computed_selection;
ext.computed_scrollbar = computed_scrollbar;
ext.computed_scrollbar_thumb = computed_scrollbar_thumb;
ext.style_dirty = false;
if layout_changed {
ext.layout_dirty = true;
}
}
let mut flags = SubtreeFlags {
has_positioned_pseudo: own_has_positioned_pseudo,
has_collapse: computed.border_collapse == crate::layout::BorderCollapse::Collapse,
};
for child in child_ids {
flags.merge(cascade_subtree(dom, sheets, merged_vars, child, &computed));
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.tree_has_positioned_pseudo = flags.has_positioned_pseudo;
ext.tree_has_collapse = flags.has_collapse;
}
flags
}
fn compute_element_style(
dom: &Dom<TuiExt>,
sheets: &[&Stylesheet],
merged_vars: &VarMap,
id: NodeId,
parent: &ComputedStyle,
) -> ComputedStyle {
let mut working = ComputedStyle::initial();
inherit_inheritable_from(&mut working, parent);
working.vars = std::rc::Rc::clone(merged_vars);
let mut matching: Vec<(usize, &Rule)> = Vec::new();
for (sheet_idx, sheet) in sheets.iter().enumerate() {
for rule in sheet.rules() {
if rule.pseudo == PseudoElementTarget::None && dom.matches_list(id, &rule.selector) {
matching.push((sheet_idx, rule));
}
}
}
matching.sort_by_key(|(sheet_idx, r)| (r.specificity, *sheet_idx, r.source_idx));
let sorted: Vec<&Rule> = matching.iter().map(|(_, r)| *r).collect();
let inline = dom.node(id).ext().map(|e| &e.inline_style);
apply_cascade_ladder(&mut working, &sorted, inline, parent);
let attr_lookup = |name: &str| dom.node(id).get_attribute(name).map(|s| s.to_string());
working.content = resolve_content_on(&working, &sorted, inline, &attr_lookup).unwrap_or(None);
finalize_border_fg(&mut working, &sorted, inline);
finalize_bfc_formation(&mut working);
working
}
fn compute_pseudo_style(
dom: &Dom<TuiExt>,
sheets: &[&Stylesheet],
id: NodeId,
host_computed: &ComputedStyle,
target: PseudoElementTarget,
) -> Option<ComputedStyle> {
if target == PseudoElementTarget::None {
return None;
}
let mut working = ComputedStyle::initial();
inherit_inheritable_from(&mut working, host_computed);
working.vars = host_computed.vars.clone();
let mut matching: Vec<(usize, &Rule)> = Vec::new();
for (sheet_idx, sheet) in sheets.iter().enumerate() {
for rule in sheet.rules() {
if rule.pseudo == target && dom.matches_list(id, &rule.selector) {
matching.push((sheet_idx, rule));
}
}
}
matching.sort_by_key(|(sheet_idx, r)| (r.specificity, *sheet_idx, r.source_idx));
let sorted: Vec<&Rule> = matching.iter().map(|(_, r)| *r).collect();
apply_cascade_ladder(&mut working, &sorted, None, host_computed);
finalize_border_fg(&mut working, &sorted, None);
finalize_bfc_formation(&mut working);
let attr_lookup = |name: &str| dom.node(id).get_attribute(name).map(|s| s.to_string());
let declared = resolve_content_on(&working, &sorted, None, &attr_lookup);
let fallback = dom.node(id).ext().and_then(|e| match target {
PseudoElementTarget::Before => e.before_content.clone(),
PseudoElementTarget::After => e.after_content.clone(),
PseudoElementTarget::Backdrop
| PseudoElementTarget::Selection
| PseudoElementTarget::Scrollbar
| PseudoElementTarget::ScrollbarThumb
| PseudoElementTarget::None => None,
});
let final_content = match declared {
Some(explicit) => explicit, None => fallback, };
if sorted.is_empty() && final_content.is_none() {
return None;
}
working.content = final_content;
Some(working)
}