use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::{MarginChainMemo, TuiExt};
use crate::layout::{Flow, MarginValue, Size};
use crate::render::inline::generated::{inline_content_at_edge, own_line_pseudos};
use crate::style::ComputedStyle;
use super::super::is_in_flow;
use super::width::block_content_width;
pub(super) fn is_empty_collapse_through(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
resolved_height: u16,
) -> bool {
resolved_height == 0 && is_collapse_through_shape(dom, id, computed)
}
fn is_statically_empty_collapse_through(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
) -> bool {
matches!(computed.height, Size::Fixed(0) | Size::Auto)
&& is_collapse_through_shape(dom, id, computed)
}
fn is_collapse_through_shape(dom: &Dom<TuiExt>, id: NodeId, computed: &ComputedStyle) -> bool {
if !computed.padding.top.is_zero() || !computed.padding.bottom.is_zero() {
return false;
}
if computed.border.top.is_visible() || computed.border.bottom.is_visible() {
return false;
}
if let Some(crate::layout::MinSize::Cells(n)) = computed.min_height
&& n > 0
{
return false;
}
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element if is_in_flow(dom, child.id()) => return false,
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
return false;
}
}
_ => {}
}
}
true
}
pub(super) fn parent_collapses_top_with_first_child(
dom: &Dom<TuiExt>,
id: NodeId,
parent: &ComputedStyle,
) -> bool {
parent.padding.top.is_zero()
&& parent.border.top.is_none()
&& !establishes_independent_formatting_context(dom, id, parent)
&& !inline_content_at_edge(dom, id, false)
&& !own_line_pseudos(dom, id).before
}
pub(super) fn parent_collapses_bottom_with_last_child(
dom: &Dom<TuiExt>,
id: NodeId,
parent: &ComputedStyle,
) -> bool {
parent.padding.bottom.is_zero()
&& parent.border.bottom.is_none()
&& !establishes_independent_formatting_context(dom, id, parent)
&& !inline_content_at_edge(dom, id, true)
&& !own_line_pseudos(dom, id).after
}
fn establishes_independent_formatting_context(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
) -> bool {
if computed.establishes_new_bfc || id == dom.root() {
return true;
}
let mut parent = dom.node(id).parent_node();
while let Some(p) = parent {
if p.id() == dom.root() {
return p.node_type() == NodeType::Fragment
|| p.ext()
.and_then(|e| e.computed.as_ref())
.is_some_and(|c| c.flow == Flow::Flex);
}
match p.node_type() {
NodeType::Fragment => parent = p.parent_node(),
_ => {
return p
.ext()
.and_then(|e| e.computed.as_ref())
.is_some_and(|c| c.flow == Flow::Flex);
}
}
}
false
}
#[derive(Debug, Default, Clone, Copy)]
pub(super) struct MarginAccumulator {
pub(super) positive_max: i16,
pub(super) negative_min: i16,
}
impl MarginAccumulator {
pub(super) fn new() -> Self {
Self::default()
}
pub(super) fn add(&mut self, margin: i16) {
if margin > self.positive_max {
self.positive_max = margin;
}
if margin < self.negative_min {
self.negative_min = margin;
}
}
pub(super) fn resolved(&self) -> i16 {
self.positive_max + self.negative_min
}
pub(super) fn merge(&mut self, other: Self) {
if other.positive_max > self.positive_max {
self.positive_max = other.positive_max;
}
if other.negative_min < self.negative_min {
self.negative_min = other.negative_min;
}
}
}
type ChainEntry = (
NodeId,
u16,
Option<MarginAccumulator>,
Option<MarginAccumulator>,
);
pub(super) fn store_margin_chain_memo(dom: &mut Dom<TuiExt>, entries: &[ChainEntry]) {
for &(id, cb, top, bottom) in entries {
let mut node = dom.node_mut(id);
let Some(ext) = node.ext_mut() else {
continue;
};
let mut memo = match ext.margin_chain {
Some(m) if m.containing_block_width == cb => m,
_ => MarginChainMemo {
containing_block_width: cb,
outer_top: None,
outer_bottom: None,
},
};
if let Some(t) = top {
memo.outer_top = Some((t.positive_max, t.negative_min));
}
if let Some(b) = bottom {
memo.outer_bottom = Some((b.positive_max, b.negative_min));
}
ext.margin_chain = Some(memo);
}
}
fn memoized_chain(
dom: &Dom<TuiExt>,
id: NodeId,
containing_block_width: u16,
pick: impl Fn(&MarginChainMemo) -> Option<(i16, i16)>,
) -> Option<MarginAccumulator> {
let memo = dom.node(id).ext()?.margin_chain?;
if memo.containing_block_width != containing_block_width {
return None;
}
pick(&memo).map(|(positive_max, negative_min)| MarginAccumulator {
positive_max,
negative_min,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Edge {
Top,
Bottom,
}
pub(super) fn outer_top_margin(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
containing_block_width: u16,
memo: &mut Vec<ChainEntry>,
) -> MarginAccumulator {
outer_edge_margin(dom, id, computed, containing_block_width, memo, Edge::Top)
}
pub(super) fn outer_bottom_margin(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
containing_block_width: u16,
memo: &mut Vec<ChainEntry>,
) -> MarginAccumulator {
outer_edge_margin(
dom,
id,
computed,
containing_block_width,
memo,
Edge::Bottom,
)
}
fn outer_edge_margin(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
containing_block_width: u16,
memo: &mut Vec<ChainEntry>,
edge: Edge,
) -> MarginAccumulator {
let pick = |m: &MarginChainMemo| match edge {
Edge::Top => m.outer_top,
Edge::Bottom => m.outer_bottom,
};
if let Some(acc) = memoized_chain(dom, id, containing_block_width, pick) {
return acc;
}
let (own, collapses) = match edge {
Edge::Top => (
&computed.margin.top,
parent_collapses_top_with_first_child(dom, id, computed),
),
Edge::Bottom => (
&computed.margin.bottom,
parent_collapses_bottom_with_last_child(dom, id, computed),
),
};
let mut acc = MarginAccumulator::new();
acc.add(vertical_margin(own, containing_block_width));
if collapses {
let child_cb = block_content_width(computed, containing_block_width);
let children: Vec<_> = dom.node(id).child_nodes().collect();
let ordered: Box<dyn Iterator<Item = _>> = match edge {
Edge::Top => Box::new(children.into_iter()),
Edge::Bottom => Box::new(children.into_iter().rev()),
};
for child in ordered {
if !is_in_flow(dom, child.id()) {
continue;
}
match child.node_type() {
NodeType::Element => {
let child_computed = child
.ext()
.and_then(|e| e.computed.clone())
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
use crate::layout::Display;
if matches!(
child_computed.display,
Display::Inline | Display::InlineBlock
) {
break;
}
acc.merge(outer_edge_margin(
dom,
child.id(),
&child_computed,
child_cb,
memo,
edge,
));
if is_statically_empty_collapse_through(dom, child.id(), &child_computed) {
let other = match edge {
Edge::Top => &child_computed.margin.bottom,
Edge::Bottom => &child_computed.margin.top,
};
acc.add(vertical_margin(other, child_cb));
continue;
}
break;
}
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
break; }
}
_ => {}
}
}
}
let (top, bottom) = match edge {
Edge::Top => (Some(acc), None),
Edge::Bottom => (None, Some(acc)),
};
memo.push((id, containing_block_width, top, bottom));
acc
}
fn vertical_margin(m: &MarginValue, cb_width: u16) -> i16 {
match m {
MarginValue::Auto => 0,
MarginValue::Cells(n) => *n,
MarginValue::Calc(_) => m.resolve(cb_width),
}
}
#[cfg(debug_assertions)]
pub(crate) fn debug_assert_no_margin_chain_memo(dom: &Dom<TuiExt>, id: NodeId) {
if let Some(ext) = dom.node(id).ext() {
debug_assert!(
ext.margin_chain.is_none(),
"margin-chain memo survived the layout pass on {id:?}"
);
}
for child in dom.node(id).child_nodes() {
debug_assert_no_margin_chain_memo(dom, child.id());
}
}