use rdom_core::{Dom, NodeId};
use crate::ext::TuiExt;
use crate::layout::{Direction, LayoutRect, Size, clamp_size};
use crate::node::TuiNodeExt;
use crate::render::inline::compute_inline_layout;
use crate::style::ComputedStyle;
use super::ifc::is_ifc_block;
use super::intrinsic::{content_min_size, intrinsic_size};
use super::{element_children_of, layout_node, parent_scroll};
pub(super) fn layout_children(
dom: &mut Dom<TuiExt>,
id: NodeId,
container: LayoutRect,
computed: &ComputedStyle,
) -> Option<super::block::BlockMeasurement> {
if is_ifc_block(dom, id) {
for child in element_children_of(dom, id) {
if let Some(ext) = dom.node_mut(child).ext_mut() {
ext.layout = LayoutRect::new(container.x, container.y, 0, 0);
ext.content_layout = ext.layout;
ext.layout_dirty = false;
}
}
let inline_layout = compute_inline_layout(dom, id, container.width);
let mut atoms: Vec<(NodeId, LayoutRect)> = Vec::new();
for (line_idx, line) in inline_layout.lines.iter().enumerate() {
let line_y = container.y + line_idx as i32;
for fragment in &line.fragments {
if !fragment.atomic {
continue;
}
atoms.push((
fragment.node,
LayoutRect::new(container.x + fragment.x as i32, line_y, fragment.width, 1),
));
}
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.inline_layout = Some(inline_layout);
}
for (atom_id, atom_rect) in atoms {
layout_node(dom, atom_id, atom_rect);
}
return None;
}
let has_text_child = dom
.node(id)
.child_nodes()
.any(|c| c.node_type() == rdom_core::NodeType::Text);
if has_text_child && element_children_of(dom, id).is_empty() {
let inline_layout = compute_inline_layout(dom, id, container.width);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.inline_layout = Some(inline_layout);
}
return None;
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.inline_layout = None;
}
match computed.flow {
crate::layout::Flow::Block => {
return Some(super::block::layout_block_children(
dom, id, container, computed,
));
}
crate::layout::Flow::Flex => {
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.anonymous_blocks.clear();
}
}
}
let children: Vec<NodeId> = element_children_of(dom, id)
.into_iter()
.filter(|&c| {
let computed = dom.node(c).ext().and_then(|e| e.computed.as_ref());
match computed {
Some(s) => {
s.display != crate::layout::Display::None
&& !matches!(
s.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
)
}
None => true,
}
})
.collect();
layout_flex_children(dom, &children, container, computed);
None
}
pub(super) fn layout_flex_children(
dom: &mut Dom<TuiExt>,
children: &[NodeId],
container: LayoutRect,
parent: &ComputedStyle,
) {
if children.is_empty() {
return;
}
let direction = parent.direction;
let gap = parent.gap;
let (top_inset, bot_inset, left_inset, right_inset) =
collapse_parent_edge_insets(dom, children, parent);
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 main_budget: u16 = match direction {
Direction::Row => container.width,
Direction::Column => container.height,
};
let cross_budget: u16 = match direction {
Direction::Row => container.height,
Direction::Column => container.width,
};
let mut child_info: Vec<ChildMain> = Vec::with_capacity(children.len());
let mut consumed_fixed: u16 = 0;
let mut total_flex_weight: u32 = 0;
let mut auto_main_count: u32 = 0;
for &child in children {
let c = dom
.node(child)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
let (main_size, min_raw, max) = match direction {
Direction::Row => (c.width, c.min_width, c.max_width),
Direction::Column => (c.height, c.min_height, c.max_height),
};
use crate::layout::MarginValue;
let (main_start_m, main_end_m) = match direction {
Direction::Row => (c.margin.left.clone(), c.margin.right.clone()),
Direction::Column => (c.margin.top.clone(), c.margin.bottom.clone()),
};
let main_cb_w = match direction {
Direction::Row => main_budget,
Direction::Column => cross_budget,
};
let margin_consumed = match (&main_start_m, &main_end_m) {
(MarginValue::Auto, MarginValue::Auto) => 0u16,
(a, MarginValue::Auto) => a.resolve(main_cb_w).max(0) as u16,
(MarginValue::Auto, b) => b.resolve(main_cb_w).max(0) as u16,
(a, b) => (a.resolve(main_cb_w).max(0) as u16)
.saturating_add(b.resolve(main_cb_w).max(0) as u16),
};
consumed_fixed = consumed_fixed.saturating_add(margin_consumed);
if matches!(main_start_m, MarginValue::Auto) {
auto_main_count += 1;
}
if matches!(main_end_m, MarginValue::Auto) {
auto_main_count += 1;
}
let natural = match &main_size {
Size::Fixed(n) => MainNatural::Fixed(*n),
Size::Flex(w) => {
total_flex_weight += *w as u32;
MainNatural::Flex(*w)
}
Size::Percent(p) => {
let resolved = ((main_budget as u32 * *p as u32) / 100).min(u16::MAX as u32) as u16;
MainNatural::Fixed(resolved)
}
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(main_budget as i32));
let resolved = v.max(0).min(u16::MAX as i32) as u16;
MainNatural::Fixed(resolved)
}
Size::Auto => {
let intrinsic = intrinsic_size(dom, child, direction, cross_budget);
MainNatural::Auto(intrinsic)
}
};
let min = match min_raw {
None => None,
Some(crate::layout::MinSize::Cells(n)) => Some(n),
Some(crate::layout::MinSize::Auto) => None,
};
if let MainNatural::Fixed(n) | MainNatural::Auto(n) = natural {
consumed_fixed = consumed_fixed.saturating_add(n);
}
let resolve_margin = |m: MarginValue| -> MarginValue {
match m {
MarginValue::Auto => MarginValue::Auto,
MarginValue::Cells(n) => MarginValue::Cells(n),
MarginValue::Calc(_) => MarginValue::Cells(m.resolve(main_cb_w)),
}
};
child_info.push(ChildMain {
id: child,
main: natural,
min,
max,
main_start_margin: resolve_margin(main_start_m),
main_end_margin: resolve_margin(main_end_m),
});
}
let gap_total = gap.saturating_mul((children.len() as u16).saturating_sub(1));
let (edge_i, edge_next) = match direction {
Direction::Column => (CollapseEdge::Bottom, CollapseEdge::Top),
Direction::Row => (CollapseEdge::Right, CollapseEdge::Left),
};
let mut overlap_savings: u16 = 0;
if parent.border_collapse == crate::layout::BorderCollapse::Collapse && gap == 0 {
for i in 0..child_info.len().saturating_sub(1) {
if has_effective_border_on_edge(dom, child_info[i].id, edge_i)
&& has_effective_border_on_edge(dom, child_info[i + 1].id, edge_next)
{
overlap_savings = overlap_savings.saturating_add(1);
}
}
}
let remaining = main_budget
.saturating_sub(consumed_fixed)
.saturating_sub(gap_total)
.saturating_add(overlap_savings);
let auto_share: u16 = (remaining as u32).checked_div(auto_main_count).unwrap_or(0) as u16;
let auto_remainder: u32 = (remaining as u32).checked_rem(auto_main_count).unwrap_or(0);
let flex_remaining: u16 = if auto_main_count > 0 { 0 } else { remaining };
let mut final_main: Vec<u16> = {
let mut accumulated_weight: u32 = 0;
let mut accumulated_flex_size: u32 = 0;
child_info
.iter()
.map(|ci| {
let natural = match ci.main {
MainNatural::Fixed(n) | MainNatural::Auto(n) => n,
MainNatural::Flex(w) => {
accumulated_weight = accumulated_weight.saturating_add(w as u32);
let target = (flex_remaining as u32)
.saturating_mul(accumulated_weight)
.checked_div(total_flex_weight)
.unwrap_or(0);
let share = target.saturating_sub(accumulated_flex_size) as u16;
accumulated_flex_size = target;
share
}
};
clamp_size(natural, ci.min, ci.max)
})
.collect()
};
let net_budget = (main_budget as i32) - (gap_total as i32) + (overlap_savings as i32);
let total: i32 = final_main.iter().map(|&n| n as i32).sum();
if total > net_budget && net_budget > 0 {
let overflow = (total - net_budget) as u32;
let shrink_of = |ci: &ChildMain| -> u32 {
dom.node(ci.id)
.computed()
.map(|c| c.flex_shrink as u32)
.unwrap_or(1)
};
let total_shrink_basis: u32 = child_info
.iter()
.zip(final_main.iter())
.map(|(ci, &size)| (size as u32) * shrink_of(ci))
.sum();
if let Some(divisor) = std::num::NonZeroU32::new(total_shrink_basis) {
let mut accumulated_basis: u32 = 0;
let mut accumulated_shrink: u32 = 0;
for (i, ci) in child_info.iter().enumerate() {
let shrink = shrink_of(ci);
if shrink == 0 {
continue;
}
accumulated_basis += (final_main[i] as u32) * shrink;
let target_total_shrink = (accumulated_basis * overflow) / divisor.get();
let my_shrink = target_total_shrink.saturating_sub(accumulated_shrink) as u16;
accumulated_shrink = target_total_shrink;
let new_size = final_main[i].saturating_sub(my_shrink);
let floor = ci.min.unwrap_or_else(|| {
resolve_auto_min(dom, ci.id, direction, main_budget, cross_budget)
});
final_main[i] = new_size.max(floor);
}
}
}
let scroll_main = parent_scroll(dom, children, direction);
let mut main_cursor: i32 = match direction {
Direction::Row => container.x - scroll_main,
Direction::Column => container.y - scroll_main,
};
let child_list: Vec<(NodeId, u16)> = child_info
.iter()
.map(|ci| ci.id)
.zip(final_main.iter().copied())
.collect();
let mut autos_consumed: u32 = 0;
let resolve_auto = |consumed: &mut u32| -> u16 {
let extra = if *consumed < auto_remainder { 1 } else { 0 };
*consumed += 1;
auto_share.saturating_add(extra)
};
for (i, (child_id, size)) in child_list.iter().enumerate() {
let child_computed = dom
.node(*child_id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
use crate::layout::MarginValue;
let main_start_cells = match &child_info[i].main_start_margin {
MarginValue::Cells(n) => (*n).max(0) as u16,
MarginValue::Auto => resolve_auto(&mut autos_consumed),
MarginValue::Calc(_) => unreachable!("Calc pre-resolved to Cells"),
};
let main_end_cells = match &child_info[i].main_end_margin {
MarginValue::Cells(n) => (*n).max(0) as u16,
MarginValue::Auto => resolve_auto(&mut autos_consumed),
MarginValue::Calc(_) => unreachable!("Calc pre-resolved to Cells"),
};
main_cursor = main_cursor.saturating_add(main_start_cells as i32);
let main_was_auto = matches!(child_info[i].main, MainNatural::Auto(_));
let cross_size = resolve_cross_size(
dom,
*child_id,
&child_computed,
cross_budget,
direction,
*size,
main_was_auto,
);
let child_rect = match direction {
Direction::Row => LayoutRect::new(main_cursor, container.y, *size, cross_size),
Direction::Column => LayoutRect::new(container.x, main_cursor, cross_size, *size),
};
layout_node(dom, *child_id, child_rect);
main_cursor = main_cursor.saturating_add(*size as i32);
main_cursor = main_cursor.saturating_add(main_end_cells as i32);
if i + 1 < child_list.len() {
main_cursor = main_cursor.saturating_add(gap as i32);
if gap == 0
&& parent.border_collapse == crate::layout::BorderCollapse::Collapse
&& has_effective_border_on_edge(dom, child_info[i].id, edge_i)
&& has_effective_border_on_edge(dom, child_info[i + 1].id, edge_next)
{
main_cursor = main_cursor.saturating_sub(1);
}
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(super) enum CollapseEdge {
Top,
Bottom,
Left,
Right,
}
pub(super) fn has_effective_border_on_edge(
dom: &Dom<TuiExt>,
id: NodeId,
edge: CollapseEdge,
) -> bool {
let computed = dom
.node(id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
match edge {
CollapseEdge::Top => !computed.border.top.is_none(),
CollapseEdge::Bottom => !computed.border.bottom.is_none(),
CollapseEdge::Left => !computed.border.left.is_none(),
CollapseEdge::Right => !computed.border.right.is_none(),
}
}
pub(super) fn collapse_parent_edge_insets(
dom: &Dom<TuiExt>,
children: &[NodeId],
parent: &ComputedStyle,
) -> (u16, u16, u16, u16) {
use crate::layout::BorderCollapse;
if parent.border_collapse != BorderCollapse::Collapse {
return (0, 0, 0, 0);
}
let parent_has_top = !parent.border.top.is_none();
let parent_has_bottom = !parent.border.bottom.is_none();
let parent_has_left = !parent.border.left.is_none();
let parent_has_right = !parent.border.right.is_none();
if !(parent_has_top || parent_has_bottom || parent_has_left || parent_has_right) {
return (0, 0, 0, 0);
}
let first = *children.first().unwrap();
let last = *children.last().unwrap();
let needs_inset =
|id: NodeId, edge: CollapseEdge| -> bool { !has_effective_border_on_edge(dom, id, edge) };
let (top, bottom, left, right) = match parent.direction {
Direction::Column => {
let top = if parent_has_top && needs_inset(first, CollapseEdge::Top) {
1
} else {
0
};
let bottom = if parent_has_bottom && needs_inset(last, CollapseEdge::Bottom) {
1
} else {
0
};
let left = if parent_has_left && needs_inset(first, CollapseEdge::Left) {
1
} else {
0
};
let right = if parent_has_right && needs_inset(first, CollapseEdge::Right) {
1
} else {
0
};
(top, bottom, left, right)
}
Direction::Row => {
let left = if parent_has_left && needs_inset(first, CollapseEdge::Left) {
1
} else {
0
};
let right = if parent_has_right && needs_inset(last, CollapseEdge::Right) {
1
} else {
0
};
let top = if parent_has_top && needs_inset(first, CollapseEdge::Top) {
1
} else {
0
};
let bottom = if parent_has_bottom && needs_inset(first, CollapseEdge::Bottom) {
1
} else {
0
};
(top, bottom, left, right)
}
};
(top, bottom, left, right)
}
fn aspect_cross_from_main(
main: u16,
ratio: crate::layout::AspectRatio,
direction: Direction,
) -> u16 {
let r = ratio.as_f32();
let cross_f = match direction {
Direction::Row => (main as f32) / r,
Direction::Column => (main as f32) * r,
};
if cross_f.is_finite() {
cross_f.max(0.0).round_ties_even() as u16
} else {
0
}
}
struct ChildMain {
id: NodeId,
main: MainNatural,
min: Option<u16>,
max: Option<u16>,
main_start_margin: crate::layout::MarginValue,
main_end_margin: crate::layout::MarginValue,
}
fn resolve_auto_min(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
main_budget: u16,
cross_budget: u16,
) -> u16 {
let computed = match dom.node(id).computed() {
Some(c) => c.clone(),
None => return 0,
};
let main_size = match direction {
Direction::Row => &computed.width,
Direction::Column => &computed.height,
};
let overflow_on_axis = match direction {
Direction::Row => computed.overflow_x,
Direction::Column => computed.overflow_y,
};
if overflow_on_axis != crate::layout::Overflow::Visible {
return 0;
}
let specified_cap: Option<u16> = match main_size {
Size::Fixed(n) => Some(*n),
Size::Percent(p) => {
Some(((main_budget as u32 * *p as u32) / 100).min(u16::MAX as u32) as u16)
}
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(main_budget as i32));
Some(v.max(0).min(u16::MAX as i32) as u16)
}
Size::Flex(_) => Some(0),
Size::Auto => None,
};
if matches!(specified_cap, Some(0)) {
return 0;
}
let content = content_min_size(dom, id, direction, cross_budget);
match specified_cap {
Some(cap) => content.min(cap),
None => content,
}
}
enum MainNatural {
Fixed(u16),
Flex(u16),
Auto(u16),
}
fn resolve_cross_size(
dom: &Dom<TuiExt>,
child_id: NodeId,
computed: &ComputedStyle,
container_cross: u16,
direction: Direction,
main_size: u16,
main_was_auto: bool,
) -> u16 {
let (cross_size, min_raw, max) = match direction {
Direction::Row => (&computed.height, computed.min_height, computed.max_height),
Direction::Column => (&computed.width, computed.min_width, computed.max_width),
};
let cross_dir = match direction {
Direction::Row => Direction::Column,
Direction::Column => Direction::Row,
};
let natural = match cross_size {
Size::Fixed(n) => *n,
Size::Flex(_) => container_cross,
Size::Percent(p) => {
((container_cross as u32 * *p as u32) / 100).min(u16::MAX as u32) as u16
}
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(container_cross as i32));
v.max(0).min(u16::MAX as i32) as u16
}
Size::Auto => {
if let Some(ratio) = computed.aspect_ratio
&& !main_was_auto
&& main_size > 0
{
aspect_cross_from_main(main_size, ratio, direction)
} else if computed.display == crate::layout::Display::InlineBlock {
intrinsic_size(dom, child_id, cross_dir, container_cross)
} else {
container_cross
}
}
};
let min = match min_raw {
None => None,
Some(crate::layout::MinSize::Cells(n)) => Some(n),
Some(crate::layout::MinSize::Auto) => {
Some(intrinsic_size(dom, child_id, cross_dir, container_cross))
}
};
clamp_size(natural, min, max)
}