use std::rc::Rc;
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;
pub(super) use super::counters::CounterState;
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)
}
pub(super) fn parent_computed_for(
dom: &Dom<TuiExt>,
root: NodeId,
merged_vars: &VarMap,
) -> std::rc::Rc<ComputedStyle> {
dom.node(root)
.parent_node()
.and_then(|p| p.ext().and_then(|e| e.computed.clone()))
.unwrap_or_else(|| {
let mut initial = ComputedStyle::initial();
initial.vars = merged_vars.clone();
std::rc::Rc::new(initial)
})
}
pub(super) fn bubble_subtree_flags(dom: &mut Dom<TuiExt>, root: NodeId, flags: SubtreeFlags) {
if !(flags.has_positioned_pseudo || flags.has_collapse) {
return;
}
let mut cur = dom.node(root).parent_node().map(|p| p.id());
while let Some(p) = cur {
if let Some(ext) = dom.node_mut(p).ext_mut() {
if flags.has_positioned_pseudo {
ext.tree_has_positioned_pseudo = true;
}
if flags.has_collapse {
ext.tree_has_collapse = true;
}
}
cur = dom.node(p).parent_node().map(|n| n.id());
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn cascade_roots_in_order(
dom: &mut Dom<TuiExt>,
sheets: &[&Stylesheet],
merged_vars: &VarMap,
roots: &[NodeId],
next: &mut usize,
id: NodeId,
counters: &mut CounterState,
) {
if *next >= roots.len() {
return;
}
if id == roots[*next] {
let parent_computed = parent_computed_for(dom, id, merged_vars);
let flags = cascade_subtree(dom, sheets, id, &parent_computed, counters);
bubble_subtree_flags(dom, id, flags);
*next += 1;
while *next < roots.len()
&& dom
.compare_document_position(id, roots[*next])
.contains(rdom_core::DocumentPosition::CONTAINED_BY)
{
*next += 1;
}
return;
}
let parent_id = dom.node(id).parent_node().map(|p| p.id());
if let Some(c) = dom.node(id).ext().and_then(|e| e.computed.as_ref()) {
counters.enter(parent_id, &c.counter_reset, &c.counter_increment);
}
let children: Vec<NodeId> = dom.node(id).child_nodes().map(|n| n.id()).collect();
for child in children {
cascade_roots_in_order(dom, sheets, merged_vars, roots, next, child, counters);
if *next >= roots.len() {
break;
}
}
counters.exit(id);
}
fn candidate_rules(dom: &Dom<TuiExt>, id: NodeId, sheet: &Stylesheet, out: &mut Vec<u32>) {
let node = dom.node(id);
sheet.rule_index().candidates(
node.tag_name(),
node.id_attr(),
node.class_list().iter(),
out,
);
}
#[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],
id: NodeId,
parent_computed: &ComputedStyle,
counters: &mut CounterState,
) -> 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,
child,
parent_computed,
counters,
));
}
counters.exit(id);
return flags;
}
let (
computed,
computed_before,
computed_backdrop,
computed_selection,
computed_scrollbar,
computed_scrollbar_thumb_vertical,
computed_scrollbar_thumb_horizontal,
) = {
let parent_id = dom.node(id).parent_node().map(|p| p.id());
let computed = compute_element_style(dom, sheets, id, parent_computed, parent_id, counters);
let cb = compute_pseudo_style_layered(
dom,
sheets,
id,
&computed,
before_targets(dom, id),
counters,
);
let cbd = compute_pseudo_style(
dom,
sheets,
id,
&computed,
PseudoElementTarget::Backdrop,
counters,
);
let csel = compute_pseudo_style(
dom,
sheets,
id,
&computed,
PseudoElementTarget::Selection,
counters,
);
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_v, csbt_h) = if needs_scrollbar {
(
compute_pseudo_style(
dom,
sheets,
id,
&computed,
PseudoElementTarget::Scrollbar,
counters,
),
compute_pseudo_style_layered(
dom,
sheets,
id,
&computed,
&PseudoElementTarget::thumb_targets(true),
counters,
),
compute_pseudo_style_layered(
dom,
sheets,
id,
&computed,
&PseudoElementTarget::thumb_targets(false),
counters,
),
)
} else {
(None, None, None)
};
(computed, cb, cbd, csel, csb, csbt_v, csbt_h)
};
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(std::rc::Rc::new(computed.clone()));
ext.computed_backdrop = computed_backdrop.map(Rc::new);
ext.computed_selection = computed_selection.map(Rc::new);
ext.computed_scrollbar = computed_scrollbar.map(Rc::new);
ext.computed_scrollbar_thumb_vertical = computed_scrollbar_thumb_vertical.map(Rc::new);
ext.computed_scrollbar_thumb_horizontal = computed_scrollbar_thumb_horizontal.map(Rc::new);
ext.style_dirty = false;
if layout_changed {
ext.layout_dirty = true;
}
}
let mut flags = SubtreeFlags {
has_positioned_pseudo: false,
has_collapse: computed.border_collapse == crate::layout::BorderCollapse::Collapse,
};
for child in child_ids {
flags.merge(cascade_subtree(dom, sheets, child, &computed, counters));
}
let computed_after = compute_pseudo_style(
dom,
sheets,
id,
&computed,
PseudoElementTarget::After,
counters,
);
counters.exit(id);
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);
flags.has_positioned_pseudo |= own_has_positioned_pseudo;
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.computed_before = computed_before.map(std::rc::Rc::new);
ext.computed_after = computed_after.map(std::rc::Rc::new);
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],
id: NodeId,
parent: &ComputedStyle,
parent_id: Option<NodeId>,
counters: &mut CounterState,
) -> ComputedStyle {
let mut working = ComputedStyle::initial();
inherit_inheritable_from(&mut working, parent);
let mut matching: Vec<(usize, &Rule)> = Vec::new();
let mut candidates = Vec::new();
for (sheet_idx, sheet) in sheets.iter().enumerate() {
candidate_rules(dom, id, sheet, &mut candidates);
for &ri in &candidates {
let rule = &sheet.rules()[ri as usize];
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().and_then(|e| e.inline_style.as_deref());
apply_cascade_ladder(&mut working, &sorted, inline, parent);
counters.enter(
parent_id,
&working.counter_reset,
&working.counter_increment,
);
let attr_lookup = |name: &str| dom.node(id).get_attribute(name).map(|s| s.to_string());
let counter_lookup = |name: &str| counters.value(name);
working.content = resolve_content_on(&working, &sorted, inline, &attr_lookup, &counter_lookup)
.unwrap_or(None);
finalize_border_fg(&mut working, &sorted, inline);
finalize_bfc_formation(&mut working);
working
}
fn before_targets(dom: &Dom<TuiExt>, id: NodeId) -> &'static [PseudoElementTarget] {
const BEFORE: &[PseudoElementTarget] = &[PseudoElementTarget::Before];
const PLACEHOLDER: &[PseudoElementTarget] = &[
PseudoElementTarget::Before,
PseudoElementTarget::Placeholder,
];
let node = dom.node(id);
let control = matches!(node.tag_name(), Some("input" | "textarea"));
if control && dom.is_placeholder_shown(id) {
PLACEHOLDER
} else {
BEFORE
}
}
fn compute_pseudo_style(
dom: &Dom<TuiExt>,
sheets: &[&Stylesheet],
id: NodeId,
host_computed: &ComputedStyle,
target: PseudoElementTarget,
counters: &mut CounterState,
) -> Option<ComputedStyle> {
compute_pseudo_style_layered(dom, sheets, id, host_computed, &[target], counters)
}
fn compute_pseudo_style_layered(
dom: &Dom<TuiExt>,
sheets: &[&Stylesheet],
id: NodeId,
host_computed: &ComputedStyle,
targets: &[PseudoElementTarget],
counters: &mut CounterState,
) -> Option<ComputedStyle> {
let target = targets[0];
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, usize, &Rule)> = Vec::new();
let mut candidates = Vec::new();
for (sheet_idx, sheet) in sheets.iter().enumerate() {
candidate_rules(dom, id, sheet, &mut candidates);
for &ri in &candidates {
let rule = &sheet.rules()[ri as usize];
if let Some(rank) = targets.iter().position(|t| *t == rule.pseudo)
&& dom.matches_list(id, &rule.selector)
{
matching.push((rank, sheet_idx, rule));
}
}
}
matching.sort_by_key(|(rank, sheet_idx, r)| (r.specificity, *rank, *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);
counters.enter(Some(id), &working.counter_reset, &working.counter_increment);
let attr_lookup = |name: &str| dom.node(id).get_attribute(name).map(|s| s.to_string());
let counter_lookup = |name: &str| counters.value(name);
let declared = resolve_content_on(&working, &sorted, None, &attr_lookup, &counter_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::ScrollbarThumbVertical
| PseudoElementTarget::ScrollbarThumbHorizontal
| PseudoElementTarget::Placeholder
| PseudoElementTarget::None => 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)
}