mod height;
mod margin_collapse;
mod width;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::{AnonymousIfc, TuiExt};
use crate::layout::{Direction, LayoutRect};
use crate::node::TuiNodeExt;
use crate::render::inline::{RunPseudos, pack_run};
use crate::style::ComputedStyle;
use super::is_in_flow;
use super::layout_node;
pub(super) use height::nearest_block_ancestor_height_is_definite;
use height::resolve_block_height;
#[cfg(debug_assertions)]
pub(super) use margin_collapse::debug_assert_no_margin_chain_memo;
use margin_collapse::{
MarginAccumulator, is_empty_collapse_through, outer_bottom_margin, outer_top_margin,
parent_collapses_bottom_with_last_child, parent_collapses_top_with_first_child,
store_margin_chain_memo,
};
use width::resolve_block_width;
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct BlockMeasurement {
pub content_height: u16,
}
pub(super) fn layout_block_children(
dom: &mut Dom<TuiExt>,
id: NodeId,
container: LayoutRect,
parent_computed: &ComputedStyle,
) -> BlockMeasurement {
let raw_children: Vec<NodeId> = dom.node(id).child_nodes().map(|c| c.id()).collect();
if raw_children.is_empty() {
return BlockMeasurement::default();
}
let in_flow: Vec<(usize, NodeId)> = raw_children
.iter()
.copied()
.enumerate()
.filter(|(_, c)| is_in_flow(dom, *c))
.collect();
let (static_before, static_trailing) = super::positioning::static_anchors(dom, &raw_children);
if in_flow.is_empty() {
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.anonymous_blocks.clear();
}
let (scroll_x, scroll_y) = dom
.node(id)
.ext()
.map_or((0, 0), |e| (e.scroll_x as i32, e.scroll_y as i32));
for &n in &static_trailing {
super::positioning::record_static_position(
dom,
n,
container.x - scroll_x,
container.y - scroll_y,
);
}
return BlockMeasurement::default();
}
let mut runs: Vec<Run> = Vec::new();
for (orig_idx, child_id) in &in_flow {
let kind = child_level(dom, *child_id);
match runs.last_mut() {
Some(last) if last.kind == kind => {
last.children.push(*child_id);
last.child_range.1 = orig_idx + 1;
}
_ => runs.push(Run {
kind,
children: vec![*child_id],
child_range: (*orig_idx, orig_idx + 1),
}),
}
}
let own_line = crate::render::inline::generated::own_line_pseudos(dom, id);
if own_line.before && runs.first().is_some_and(|r| r.kind == RunKind::Block) {
let at = runs[0].child_range.0;
runs.insert(0, Run::pseudo_only(at));
}
if own_line.after && runs.last().is_some_and(|r| r.kind == RunKind::Block) {
let at = runs[runs.len() - 1].child_range.1;
runs.push(Run::pseudo_only(at));
}
let in_flow_ids: Vec<NodeId> = in_flow.iter().map(|(_, id)| *id).collect();
let (top_inset, bot_inset, left_inset, right_inset) =
super::border_collapse::collapse_parent_edge_insets(dom, &in_flow_ids, parent_computed);
let container = LayoutRect::new(
container.x + left_inset as i32,
container.y + top_inset as i32,
container.width.saturating_sub(left_inset + right_inset),
container.height.saturating_sub(top_inset + bot_inset),
);
let containing_block_width = container.width;
let scroll_y = super::parent_scroll(dom, &in_flow_ids, crate::layout::Direction::Column);
let scroll_x = super::parent_scroll(dom, &in_flow_ids, crate::layout::Direction::Row);
let content_x = container.x - scroll_x;
let mut y_cursor: i32 = container.y - scroll_y;
let mut anon_blocks: Vec<AnonymousIfc> = Vec::new();
let mut margin_acc = MarginAccumulator::new();
let suppress_first_top_margin = parent_collapses_top_with_first_child(dom, id, parent_computed);
let suppress_last_bottom_margin =
parent_collapses_bottom_with_last_child(dom, id, parent_computed);
let last_block_run_idx = runs
.iter()
.enumerate()
.rev()
.find_map(|(i, r)| (r.kind == RunKind::Block).then_some(i));
let row_gap = super::resolve_gap(parent_computed, container, Direction::Column);
let mut placed_block_count: usize = 0;
let mut prev_block_id: Option<NodeId> = None;
for (run_idx, run) in runs.iter().enumerate() {
match run.kind {
RunKind::Block => {
let is_last_block_run = Some(run_idx) == last_block_run_idx;
let last_child_idx = run.children.len() - 1;
for (i, &child) in run.children.iter().enumerate() {
if let Some(oof) = static_before.get(&child) {
let y = y_cursor + i32::from(margin_acc.resolved());
for &n in oof {
super::positioning::record_static_position(dom, n, content_x, y);
}
}
let is_first_block_placed = placed_block_count == 0;
let is_last_block_placed = is_last_block_run && i == last_child_idx;
if !is_first_block_placed && row_gap > 0 {
y_cursor += row_gap as i32;
}
if row_gap == 0
&& parent_computed.border_collapse
== crate::layout::BorderCollapse::Collapse
&& let Some(prev) = prev_block_id
{
let prev_bot = dom
.node(prev)
.computed()
.map(|c| !c.border.bottom.is_none())
.unwrap_or(false);
let curr_top = dom
.node(child)
.computed()
.map(|c| !c.border.top.is_none())
.unwrap_or(false);
if prev_bot && curr_top {
y_cursor -= 1;
}
}
y_cursor = lay_out_block_child(
dom,
child,
BlockPlace {
container: LayoutRect::new(
content_x,
container.y,
container.width,
container.height,
),
containing_block_width,
y_cursor,
margin_acc: &mut margin_acc,
suppress_top_margin: is_first_block_placed && suppress_first_top_margin,
suppress_bottom_margin: is_last_block_placed
&& suppress_last_bottom_margin,
},
);
placed_block_count += 1;
prev_block_id = Some(child);
}
}
RunKind::Inline => {
let pseudos = RunPseudos {
before: run_idx == 0,
after: run_idx == runs.len() - 1,
};
let inline_layout =
pack_run(dom, id, &run.children, pseudos, containing_block_width);
let height = inline_layout.height();
let resolved_gap = margin_acc.resolved();
margin_acc = MarginAccumulator::new();
let anon_y = y_cursor + resolved_gap as i32;
let rect = LayoutRect::new(content_x, anon_y, containing_block_width, height);
layout_atomic_inline_blocks(dom, &inline_layout, rect);
for c in &run.children {
if let Some(oof) = static_before.get(c) {
for &n in oof {
let (x, y) = super::positioning::static_position_in_ifc(
dom,
id,
n,
&inline_layout,
rect,
);
super::positioning::record_static_position(dom, n, x, y);
}
}
}
anon_blocks.push(AnonymousIfc {
rect,
inline_layout,
child_range: run.child_range,
});
y_cursor = anon_y + height as i32;
prev_block_id = None;
}
}
}
let last_run_is_inline = matches!(runs.last().map(|r| r.kind), Some(RunKind::Inline));
if !static_trailing.is_empty() {
let mut trailing_margin = margin_acc;
if suppress_last_bottom_margin
&& !last_run_is_inline
&& let Some(&last) = runs.last().and_then(|r| r.children.last())
{
let last_computed = dom
.node(last)
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
let mut memo = Vec::new();
trailing_margin.merge(outer_bottom_margin(
dom,
last,
&last_computed,
containing_block_width,
&mut memo,
));
}
let below_last_block = y_cursor + i32::from(trailing_margin.resolved());
for &n in &static_trailing {
let (x, y) = match anon_blocks.last() {
Some(anon) if last_run_is_inline => super::positioning::static_position_in_ifc(
dom,
id,
n,
&anon.inline_layout,
anon.rect,
),
_ => (content_x, below_last_block),
};
super::positioning::record_static_position(dom, n, x, y);
}
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.anonymous_blocks = anon_blocks;
}
let initial_cursor = container.y - scroll_y;
let mut content_height = (y_cursor - initial_cursor).max(0);
if !suppress_last_bottom_margin {
content_height = (content_height + margin_acc.resolved() as i32).max(0);
}
BlockMeasurement {
content_height: content_height.min(u16::MAX as i32) as u16,
}
}
fn layout_atomic_inline_blocks(
dom: &mut Dom<TuiExt>,
inline_layout: &crate::render::inline::InlineLayout,
anon_rect: LayoutRect,
) {
for (id, rect) in crate::render::inline::atomic_placements(inline_layout, anon_rect) {
layout_node(dom, id, rect, anon_rect.width);
}
}
struct BlockPlace<'a> {
container: LayoutRect,
containing_block_width: u16,
y_cursor: i32,
margin_acc: &'a mut MarginAccumulator,
suppress_top_margin: bool,
suppress_bottom_margin: bool,
}
fn lay_out_block_child(dom: &mut Dom<TuiExt>, child: NodeId, ctx: BlockPlace<'_>) -> i32 {
let BlockPlace {
container,
containing_block_width,
y_cursor,
margin_acc,
suppress_top_margin,
suppress_bottom_margin,
} = ctx;
let computed = dom
.node(child)
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
let resolved = resolve_block_width(&computed, containing_block_width);
let height = resolve_block_height(
dom,
child,
&computed,
resolved.width,
container.height,
containing_block_width,
);
let mut memo = Vec::new();
if !suppress_top_margin {
margin_acc.merge(outer_top_margin(
dom,
child,
&computed,
containing_block_width,
&mut memo,
));
}
let mut outer_bottom = MarginAccumulator::new();
if !suppress_bottom_margin {
outer_bottom =
outer_bottom_margin(dom, child, &computed, containing_block_width, &mut memo);
}
store_margin_chain_memo(dom, &memo);
let collapse_through = is_empty_collapse_through(dom, child, &computed, height);
let outer_x = container.x + resolved.margin_left as i32;
let gap = margin_acc.resolved();
let outer_y = y_cursor + gap as i32;
let outer_rect = LayoutRect::new(outer_x, outer_y, resolved.width, height);
layout_node(dom, child, outer_rect, containing_block_width);
let actual_height = dom
.node(child)
.layout_rect()
.map(|r| r.height)
.unwrap_or(height);
if collapse_through {
margin_acc.merge(outer_bottom);
y_cursor
} else {
*margin_acc = outer_bottom;
outer_y + actual_height as i32
}
}
struct Run {
kind: RunKind,
children: Vec<NodeId>,
child_range: (usize, usize),
}
impl Run {
fn pseudo_only(at: usize) -> Self {
Run {
kind: RunKind::Inline,
children: Vec::new(),
child_range: (at, at),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RunKind {
Block,
Inline,
}
fn child_level(dom: &Dom<TuiExt>, id: NodeId) -> RunKind {
let node = dom.node(id);
match node.node_type() {
NodeType::Text => RunKind::Inline,
NodeType::Element => {
let display = node
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.display)
.unwrap_or(crate::layout::Display::Block);
match display {
crate::layout::Display::Inline | crate::layout::Display::InlineBlock => {
RunKind::Inline
}
crate::layout::Display::Block | crate::layout::Display::None => RunKind::Block,
}
}
_ => RunKind::Inline,
}
}