use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::{AnonymousIfc, TuiExt};
use crate::layout::{
Direction, LayoutRect, MarginValue, Size, clamp_size, compute_content_area_collapsed,
};
use crate::node::TuiNodeExt;
use crate::render::inline::compute_inline_layout_for_run;
use crate::render::layout_pass::intrinsic::intrinsic_size;
use crate::style::ComputedStyle;
use super::layout_node;
#[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();
if in_flow.is_empty() {
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.anonymous_blocks.clear();
}
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 in_flow_ids: Vec<NodeId> = in_flow.iter().map(|(_, id)| *id).collect();
let (top_inset, bot_inset, left_inset, right_inset) =
super::flex::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 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(parent_computed);
let suppress_last_bottom_margin = parent_collapses_bottom_with_last_child(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 = parent_computed.gap;
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() {
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,
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 inline_layout =
compute_inline_layout_for_run(dom, id, &run.children, 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(container.x, anon_y, containing_block_width, height);
layout_atomic_inline_blocks(dom, &inline_layout, rect);
anon_blocks.push(AnonymousIfc {
rect,
inline_layout,
child_range: run.child_range,
});
y_cursor = anon_y + height as i32;
prev_block_id = None;
}
}
}
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,
) {
let mut atoms: Vec<(NodeId, LayoutRect)> = Vec::new();
for (line_idx, line) in inline_layout.lines.iter().enumerate() {
let line_y = anon_rect.y + line_idx as i32;
for fragment in &line.fragments {
if !fragment.atomic {
continue;
}
let atom_rect =
LayoutRect::new(anon_rect.x + fragment.x as i32, line_y, fragment.width, 1);
atoms.push((fragment.node, atom_rect));
}
}
for (id, rect) in atoms {
layout_node(dom, id, rect);
}
}
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()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
let resolved = resolve_block_width(&computed, containing_block_width);
let height = resolve_block_height(dom, child, &computed, resolved.width, container.height);
if !suppress_top_margin {
accumulate_outer_top_margin(dom, child, &computed, margin_acc);
}
let mut outer_bottom = MarginAccumulator::new();
if !suppress_bottom_margin {
accumulate_outer_bottom_margin(dom, child, &computed, &mut outer_bottom);
}
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);
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
}
}
fn is_empty_collapse_through(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
resolved_height: u16,
) -> bool {
if resolved_height != 0 {
return false;
}
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 => {
let c = child.ext().and_then(|e| e.computed.as_ref());
let in_flow = c
.map(|s| {
s.display != crate::layout::Display::None
&& !matches!(
s.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
)
})
.unwrap_or(true);
if in_flow {
return false;
}
}
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
return false;
}
}
_ => {}
}
}
true
}
fn parent_collapses_top_with_first_child(parent: &ComputedStyle) -> bool {
parent.padding.top.is_zero() && parent.border.top.is_none() && !parent.establishes_new_bfc
}
fn parent_collapses_bottom_with_last_child(parent: &ComputedStyle) -> bool {
parent.padding.bottom.is_zero() && parent.border.bottom.is_none() && !parent.establishes_new_bfc
}
#[derive(Debug, Default, Clone, Copy)]
struct MarginAccumulator {
positive_max: i16,
negative_min: i16,
}
impl MarginAccumulator {
fn new() -> Self {
Self::default()
}
fn add(&mut self, margin: i16) {
if margin > self.positive_max {
self.positive_max = margin;
}
if margin < self.negative_min {
self.negative_min = margin;
}
}
fn resolved(&self) -> i16 {
self.positive_max + self.negative_min
}
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;
}
}
}
fn accumulate_outer_top_margin(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
acc: &mut MarginAccumulator,
) {
acc.add(vertical_margin(&computed.margin.top, 0));
if !parent_collapses_top_with_first_child(computed) {
return;
}
for child in dom.node(id).child_nodes() {
if !is_in_flow(dom, child.id()) {
continue;
}
match child.node_type() {
NodeType::Element => {
let child_computed = child
.ext()
.and_then(|e| e.computed.as_ref())
.cloned()
.unwrap_or_else(ComputedStyle::initial);
use crate::layout::Display;
if matches!(
child_computed.display,
Display::Inline | Display::InlineBlock
) {
return;
}
accumulate_outer_top_margin(dom, child.id(), &child_computed, acc);
if is_statically_empty_collapse_through(dom, child.id(), &child_computed) {
acc.add(vertical_margin(&child_computed.margin.bottom, 0));
continue;
}
return;
}
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
return; }
}
_ => {}
}
}
}
fn accumulate_outer_bottom_margin(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
acc: &mut MarginAccumulator,
) {
acc.add(vertical_margin(&computed.margin.bottom, 0));
if !parent_collapses_bottom_with_last_child(computed) {
return;
}
let children: Vec<_> = dom.node(id).child_nodes().collect();
for child in children.into_iter().rev() {
if !is_in_flow(dom, child.id()) {
continue;
}
match child.node_type() {
NodeType::Element => {
let child_computed = child
.ext()
.and_then(|e| e.computed.as_ref())
.cloned()
.unwrap_or_else(ComputedStyle::initial);
use crate::layout::Display;
if matches!(
child_computed.display,
Display::Inline | Display::InlineBlock
) {
return;
}
accumulate_outer_bottom_margin(dom, child.id(), &child_computed, acc);
if is_statically_empty_collapse_through(dom, child.id(), &child_computed) {
acc.add(vertical_margin(&child_computed.margin.top, 0));
continue;
}
return;
}
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
return;
}
}
_ => {}
}
}
}
fn is_statically_empty_collapse_through(
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;
}
match computed.height {
Size::Fixed(0) | Size::Auto => {}
_ => 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 => {
let c = child.ext().and_then(|e| e.computed.as_ref());
let in_flow = c
.map(|s| {
s.display != crate::layout::Display::None
&& !matches!(
s.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
)
})
.unwrap_or(true);
if in_flow {
return false;
}
}
NodeType::Text => {
if let Some(t) = child.node_value()
&& !t.chars().all(char::is_whitespace)
{
return false;
}
}
_ => {}
}
}
true
}
struct Run {
kind: RunKind,
children: Vec<NodeId>,
child_range: (usize, usize),
}
#[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,
}
}
use super::is_in_flow;
#[derive(Debug, Clone, Copy)]
struct ResolvedWidth {
margin_left: i16,
width: u16,
#[allow(dead_code)] margin_right: i16,
}
fn resolve_block_width(computed: &ComputedStyle, containing_block_width: u16) -> ResolvedWidth {
let cb = containing_block_width as i32;
let width_decl = &computed.width;
let ml_decl = &computed.margin.left;
let mr_decl = &computed.margin.right;
let declared_width: Option<i32> = resolve_size_to_cells(width_decl, cb);
let ml_auto = matches!(ml_decl, MarginValue::Auto);
let mr_auto = matches!(mr_decl, MarginValue::Auto);
let cb_width_u16 = containing_block_width;
let ml_cells = if ml_auto {
0i32
} else {
ml_decl.resolve(cb_width_u16) as i32
};
let mr_cells = if mr_auto {
0i32
} else {
mr_decl.resolve(cb_width_u16) as i32
};
let (ml_final, width_final, mr_final): (i32, i32, i32) =
match (declared_width, ml_auto, mr_auto) {
(None, _, _) => {
let w = cb - ml_cells - mr_cells;
(ml_cells, w.max(0), mr_cells)
}
(Some(w), true, true) => {
let leftover = cb - w;
let half = leftover.div_euclid(2);
(half, w, leftover - half)
}
(Some(w), true, false) => {
let ml = cb - w - mr_cells;
(ml, w, mr_cells)
}
(Some(w), false, true) => {
let mr = cb - w - ml_cells;
(ml_cells, w, mr)
}
(Some(w), false, false) => {
let mr = cb - w - ml_cells;
(ml_cells, w, mr)
}
};
let clamped_width = clamp_width(width_final, &computed.min_width, computed.max_width, cb);
let (ml_clamped, mr_clamped) = if clamped_width != width_final {
let leftover = cb - clamped_width;
match (ml_auto, mr_auto) {
(true, true) => {
let half = leftover.div_euclid(2);
(half, leftover - half)
}
(true, false) => (leftover - mr_cells, mr_cells),
(false, true) => (ml_cells, leftover - ml_cells),
(false, false) => (ml_cells, leftover - ml_cells),
}
} else {
(ml_final, mr_final)
};
ResolvedWidth {
margin_left: ml_clamped.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
width: clamped_width.max(0).min(u16::MAX as i32) as u16,
margin_right: mr_clamped.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
}
}
fn resolve_size_to_cells(size: &Size, basis: i32) -> Option<i32> {
match size {
Size::Auto | Size::Flex(_) => None,
Size::Fixed(n) => Some(*n as i32),
Size::Percent(p) => Some((basis * *p as i32) / 100),
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(basis));
Some(v)
}
}
}
fn clamp_width(
width: i32,
min: &Option<crate::layout::MinSize>,
max: Option<u16>,
_basis: i32, ) -> i32 {
let min_cells: Option<i32> = match min {
Some(crate::layout::MinSize::Cells(n)) => Some(*n as i32),
Some(crate::layout::MinSize::Auto) | None => None, };
let max_cells = max.map(|n| n as i32);
let after_max = match max_cells {
Some(m) => width.min(m),
None => width,
};
match min_cells {
Some(m) => after_max.max(m),
None => after_max.max(0),
}
}
fn resolve_block_height(
dom: &Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
resolved_width: u16,
container_height: u16,
) -> u16 {
let parent_height_definite = nearest_block_ancestor_height_is_definite(dom, id);
let raw = match &computed.height {
Size::Auto | Size::Flex(_) => {
intrinsic_size(dom, id, Direction::Column, resolved_width)
}
Size::Fixed(n) => *n,
Size::Percent(p) => {
if parent_height_definite {
((container_height as u32 * *p as u32) / 100).min(u16::MAX as u32) as u16
} else {
intrinsic_size(dom, id, Direction::Column, resolved_width)
}
}
Size::Calc(expr) => {
if parent_height_definite {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(container_height as i32));
v.max(0).min(u16::MAX as i32) as u16
} else {
intrinsic_size(dom, id, Direction::Column, resolved_width)
}
}
};
let min_cells: Option<u16> = match computed.min_height {
Some(crate::layout::MinSize::Cells(n)) => Some(n),
Some(crate::layout::MinSize::Auto) | None => None,
};
clamp_size(raw, min_cells, computed.max_height)
}
fn nearest_block_ancestor_height_is_definite(dom: &Dom<TuiExt>, id: NodeId) -> bool {
use crate::layout::{MinSize, Position};
let mut cur = id;
loop {
let node = dom.node(cur);
let Some(parent) = node.parent_node() else {
return true;
};
let parent_id = parent.id();
let Some(parent_computed) = parent.ext().and_then(|e| e.computed.as_ref()) else {
return true; };
if matches!(
parent_computed.position,
Position::Absolute | Position::Fixed
) {
return true;
}
if let Some(MinSize::Cells(n)) = parent_computed.min_height
&& n > 0
{
return true;
}
match parent_computed.height {
Size::Fixed(_) => return true,
Size::Auto => return false,
Size::Flex(_) => {
use crate::layout::Flow;
match parent.parent_node() {
None => return true,
Some(gp) => match gp.ext().and_then(|e| e.computed.as_ref()).map(|c| c.flow) {
None => return true,
Some(Flow::Flex) => cur = parent_id,
Some(Flow::Block) => return false,
},
}
}
Size::Percent(_) | Size::Calc(_) => {
cur = parent_id;
}
}
}
}
fn vertical_margin(m: &MarginValue, cb_width: u16) -> i16 {
match m {
MarginValue::Auto => 0,
MarginValue::Cells(n) => *n,
MarginValue::Calc(_) => m.resolve(cb_width),
}
}
#[allow(dead_code)]
fn content_area(outer: LayoutRect, computed: &ComputedStyle) -> LayoutRect {
compute_content_area_collapsed(
outer,
computed.padding.clone(),
computed.border,
computed.border_collapse,
)
}