mod block;
#[cfg(test)]
mod block_tests;
mod border_collapse;
mod flex;
pub(crate) mod geometry;
mod ifc;
pub(crate) mod intrinsic;
mod positioned_pseudos;
mod positioning;
mod sticky;
#[cfg(test)]
mod tests;
use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::{Direction, LayoutRect, Overflow, compute_content_area_collapsed};
use crate::node::TuiNodeExt;
use crate::render::Rect;
use crate::style::ComputedStyle;
use flex::{layout_children, layout_flex_children};
pub(crate) use ifc::is_ifc_block;
pub trait LayoutExt {
fn layout_dom(&mut self, viewport: Rect);
}
impl LayoutExt for Dom<TuiExt> {
fn layout_dom(&mut self, viewport: Rect) {
let root = self.root();
let root_rect = LayoutRect::new(
viewport.x as i32,
viewport.y as i32,
viewport.width,
viewport.height,
);
layout_node(self, root, root_rect, root_rect.width);
positioning::place_positioned(self, root_rect);
sticky::place_sticky(self);
#[cfg(debug_assertions)]
block::debug_assert_no_margin_chain_memo(self, root);
positioned_pseudos::place_positioned_pseudos(self, root_rect);
}
}
pub(super) fn layout_node(
dom: &mut Dom<TuiExt>,
id: NodeId,
outer_rect: LayoutRect,
containing_block_width: u16,
) {
if dom.node(id).node_type() != NodeType::Element {
if dom.node(id).node_type() == NodeType::Fragment {
layout_fragment_children(dom, id, outer_rect);
}
return;
}
let computed = dom
.node(id)
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
let parent_rect = dom
.node(id)
.parent_node()
.and_then(|p| {
use crate::node::TuiNodeExt;
p.tui_ext().map(|e| e.content_layout)
})
.unwrap_or(outer_rect);
let parent_height_definite = block::nearest_block_ancestor_height_is_definite(dom, id);
let outer_rect = positioning::apply_relative_shift(
&computed,
outer_rect,
parent_rect,
parent_height_definite,
);
let content_area = compute_content_area_collapsed(
outer_rect,
computed.padding.clone(),
computed.border,
computed.border_collapse,
containing_block_width,
);
let inner = reserve_scrollbar_gutter(content_area, &computed);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.layout = outer_rect;
ext.content_layout = inner;
ext.layout_dirty = false;
ext.margin_chain = None;
}
let measurement = layout_children(dom, id, inner, &computed);
collapse_hidden_children(dom, id);
resolve_auto_height(
dom,
id,
&computed,
containing_block_width,
measurement,
content_area.height.saturating_sub(inner.height),
);
record_scroll_content_size(dom, id, inner, &computed);
use crate::layout::ScrollbarGutter;
let auto_no_stable_y = matches!(computed.overflow_y, Overflow::Auto)
&& !matches!(computed.scrollbar_gutter, ScrollbarGutter::Stable);
let auto_no_stable_x = matches!(computed.overflow_x, Overflow::Auto)
&& !matches!(computed.scrollbar_gutter, ScrollbarGutter::Stable);
if auto_no_stable_y || auto_no_stable_x {
let (overflow_y, overflow_x) = match dom.node(id).ext() {
Some(ext) => (
auto_no_stable_y && ext.scroll_content_height > ext.content_layout.height as usize,
auto_no_stable_x && ext.scroll_content_width > inner.width as usize,
),
None => (false, false),
};
if overflow_y || overflow_x {
let inner_full = compute_content_area_collapsed(
outer_rect,
computed.padding.clone(),
computed.border,
computed.border_collapse,
containing_block_width,
);
let inner_v2 =
reserve_scrollbar_gutter_forced(inner_full, &computed, overflow_y, overflow_x);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.content_layout = inner_v2;
}
let measurement = layout_children(dom, id, inner_v2, &computed);
resolve_auto_height(
dom,
id,
&computed,
containing_block_width,
measurement,
inner_full.height.saturating_sub(inner_v2.height),
);
record_scroll_content_size(dom, id, inner_v2, &computed);
}
}
if clamp_scroll_offset(dom, id, &computed) {
let final_inner = dom
.node(id)
.ext()
.map(|e| e.content_layout)
.unwrap_or(inner);
let _ = layout_children(dom, id, final_inner, &computed);
record_scroll_content_size(dom, id, final_inner, &computed);
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
crate::runtime::scrollbar::state::note_laid_out(ext);
}
}
fn record_scroll_content_size(
dom: &mut Dom<TuiExt>,
id: NodeId,
inner: LayoutRect,
computed: &ComputedStyle,
) {
let needs = matches!(
computed.overflow_x,
Overflow::Scroll | Overflow::Auto | Overflow::Hidden
) || matches!(
computed.overflow_y,
Overflow::Scroll | Overflow::Auto | Overflow::Hidden
);
if !needs {
return;
}
let (scroll_x, scroll_y) = match dom.node(id).ext() {
Some(ext) => (ext.scroll_x as i32, ext.scroll_y as i32),
None => return,
};
let mut min_x: Option<i32> = None;
let mut min_y: Option<i32> = None;
let mut max_right: i32 = 0;
let mut max_bottom: i32 = 0;
let mut any = false;
let mut extend = |rect: LayoutRect| {
let top = rect.y + scroll_y;
let left = rect.x + scroll_x;
min_x = Some(min_x.map_or(left, |m: i32| m.min(left)));
min_y = Some(min_y.map_or(top, |m: i32| m.min(top)));
max_right = max_right.max(left + rect.width as i32);
max_bottom = max_bottom.max(top + rect.height as i32);
any = true;
};
for child in element_children_of(dom, id) {
extend_scrollable_overflow(dom, child, &mut extend);
}
if let Some(ext) = dom.node(id).ext() {
if let Some(il) = ext.inline_layout.as_ref() {
let widest = il.lines.iter().map(|l| l.width as i32).max().unwrap_or(0);
let trailing_caret_line = i32::from(trailing_newline_caret_row(dom, id));
let top = inner.y;
let left = inner.x;
min_x = Some(min_x.map_or(left, |m: i32| m.min(left)));
min_y = Some(min_y.map_or(top, |m: i32| m.min(top)));
max_right = max_right.max(left + widest);
max_bottom = max_bottom.max(top + il.height() as i32 + trailing_caret_line);
any = true;
}
for anon in &ext.anonymous_blocks {
let top = anon.rect.y + scroll_y;
let left = anon.rect.x + scroll_x;
min_x = Some(min_x.map_or(left, |m: i32| m.min(left)));
min_y = Some(min_y.map_or(top, |m: i32| m.min(top)));
max_right = max_right.max(left + anon.rect.width as i32);
max_bottom = max_bottom.max(top + anon.rect.height as i32);
any = true;
}
}
let (content_w, content_h) = if any {
(
(max_right - min_x.unwrap_or(inner.x)).max(0),
(max_bottom - min_y.unwrap_or(inner.y)).max(0),
)
} else {
(0, 0)
};
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.scroll_content_width = content_w as usize;
ext.scroll_content_height = content_h as usize;
}
}
fn trailing_newline_caret_row(dom: &Dom<TuiExt>, id: NodeId) -> bool {
use crate::node::TuiNodeExt;
if !dom.node(id).is_editable() {
return false;
}
let last_text = dom
.node(id)
.child_nodes()
.filter(|c| c.node_type() == rdom_core::NodeType::Text)
.last();
last_text.is_some_and(|t| t.node_value().is_some_and(|v| v.ends_with('\n')))
}
fn clamp_scroll_offset(dom: &mut Dom<TuiExt>, id: NodeId, computed: &ComputedStyle) -> bool {
let scrolls = !matches!(computed.overflow_x, Overflow::Visible)
|| !matches!(computed.overflow_y, Overflow::Visible);
if !scrolls {
return false;
}
let Some(ext) = dom.node(id).ext() else {
return false;
};
let vp = ext.content_layout;
let max_x = ext.scroll_content_width.saturating_sub(vp.width as usize);
let max_y = ext.scroll_content_height.saturating_sub(vp.height as usize);
let new_x = ext.scroll_x.min(max_x);
let new_y = ext.scroll_y.min(max_y);
if new_x == ext.scroll_x && new_y == ext.scroll_y {
return false;
}
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.scroll_x = new_x;
ext.scroll_y = new_y;
}
true
}
fn extend_scrollable_overflow(dom: &Dom<TuiExt>, id: NodeId, extend: &mut impl FnMut(LayoutRect)) {
if !is_in_flow(dom, id) {
return;
}
let Some(ext) = dom.node(id).ext() else {
return;
};
extend(ext.layout);
for anon in &ext.anonymous_blocks {
extend(anon.rect);
}
let clips = ext.computed.as_ref().is_some_and(|c| {
!matches!(c.overflow_x, Overflow::Visible) || !matches!(c.overflow_y, Overflow::Visible)
});
if clips {
return;
}
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element => extend_scrollable_overflow(dom, child.id(), extend),
NodeType::Fragment => {
for grand in child.child_nodes() {
if grand.node_type() == NodeType::Element {
extend_scrollable_overflow(dom, grand.id(), extend);
}
}
}
_ => {}
}
}
}
fn resolve_auto_height(
dom: &mut Dom<TuiExt>,
id: NodeId,
computed: &ComputedStyle,
containing_block_width: u16,
measurement: Option<block::BlockMeasurement>,
gutter_rows: u16,
) {
let parent_is_block_flow = dom
.node(id)
.parent_node()
.and_then(|p| {
use crate::node::TuiNodeExt;
p.tui_ext()
.and_then(|e| e.computed.as_ref().map(|c| c.flow))
})
.map(|f| matches!(f, crate::layout::Flow::Block))
.unwrap_or(true);
let is_out_of_flow_positioned = matches!(
computed.position,
crate::layout::Position::Absolute | crate::layout::Position::Fixed
);
if !matches!(computed.height, crate::layout::Size::Auto)
|| !matches!(computed.flow, crate::layout::Flow::Block)
|| !parent_is_block_flow
|| is_out_of_flow_positioned
{
return;
}
let Some(measurement) = measurement.or_else(|| {
dom.node(id)
.ext()
.and_then(|e| e.inline_layout.as_ref())
.map(|il| block::BlockMeasurement {
content_height: il.height().max(1),
})
}) else {
return;
};
let content_h = crate::layout::clamp_size(
measurement.content_height,
match computed.min_height {
Some(crate::layout::MinSize::Cells(n)) => Some(n),
_ => None,
},
computed.max_height,
);
let pad = computed.padding.top.resolve(containing_block_width)
+ computed.padding.bottom.resolve(containing_block_width);
let border = computed.border.top.cells() + computed.border.bottom.cells();
let outer_h = content_h
.saturating_add(pad)
.saturating_add(border)
.saturating_add(gutter_rows);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.layout.height = outer_h;
ext.content_layout.height = content_h;
}
}
fn layout_fragment_children(dom: &mut Dom<TuiExt>, id: NodeId, container: LayoutRect) {
let children: Vec<NodeId> = element_children_of(dom, id)
.into_iter()
.filter(|&c| is_in_flow(dom, c))
.collect();
for n in positioning::out_of_flow_positioned_children(dom, id) {
positioning::record_static_position(dom, n, container.x, container.y);
}
let fallback = ComputedStyle::initial();
layout_flex_children(dom, &children, container, &fallback);
}
pub(super) fn element_children_of(dom: &Dom<TuiExt>, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
collect_element_children(dom, id, &mut out);
out
}
pub(super) fn resolve_gap(
computed: &crate::style::ComputedStyle,
container: LayoutRect,
axis: Direction,
) -> u16 {
let basis = match axis {
Direction::Row => container.width,
Direction::Column if computed.height == crate::layout::Size::Auto => 0,
Direction::Column => container.height,
};
computed.gap.resolve(basis)
}
pub(crate) fn is_in_flow(dom: &Dom<TuiExt>, id: NodeId) -> bool {
let node = dom.node(id);
if node.node_type() != NodeType::Element {
return true; }
let Some(c) = node.ext().and_then(|e| e.computed.as_ref()) else {
return true;
};
use crate::layout::{Display, Position};
c.display != Display::None && !matches!(c.position, Position::Absolute | Position::Fixed)
}
fn collapse_hidden_children(dom: &mut Dom<TuiExt>, id: NodeId) {
for child in element_children_of(dom, id) {
let hidden = dom
.node(child)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.display == crate::layout::Display::None)
.unwrap_or(false);
if hidden {
collapse_subtree_geometry(dom, child);
}
}
}
fn collapse_subtree_geometry(dom: &mut Dom<TuiExt>, id: NodeId) {
if let Some(ext) = dom.node_mut(id).ext_mut() {
if ext.layout == LayoutRect::default() && ext.content_layout == LayoutRect::default() {
return;
}
ext.layout = LayoutRect::default();
ext.content_layout = LayoutRect::default();
}
for child in element_children_of(dom, id) {
collapse_subtree_geometry(dom, child);
}
}
fn collect_element_children(dom: &Dom<TuiExt>, id: NodeId, out: &mut Vec<NodeId>) {
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element => out.push(child.id()),
NodeType::Fragment => collect_element_children(dom, child.id(), out),
_ => {}
}
}
}
pub(super) fn parent_scroll(dom: &Dom<TuiExt>, children: &[NodeId], direction: Direction) -> i32 {
let Some(&first) = children.first() else {
return 0;
};
let Some(parent) = dom.node(first).parent_node() else {
return 0;
};
let Some(ext) = parent.ext() else { return 0 };
match direction {
Direction::Row => ext.scroll_x as i32,
Direction::Column => ext.scroll_y as i32,
}
}
pub(super) fn reserve_scrollbar_gutter_forced(
inner: LayoutRect,
computed: &ComputedStyle,
force_y: bool,
force_x: bool,
) -> LayoutRect {
let (reserve_y, reserve_x) = gutter_axes(computed, force_y, force_x);
LayoutRect::new(
inner.x,
inner.y,
if reserve_y {
inner.width.saturating_sub(1)
} else {
inner.width
},
if reserve_x {
inner.height.saturating_sub(1)
} else {
inner.height
},
)
}
pub(super) fn gutter_axes(computed: &ComputedStyle, force_y: bool, force_x: bool) -> (bool, bool) {
use crate::layout::ScrollbarGutter;
let reserves = |o: Overflow, force: bool| match o {
Overflow::Scroll => true,
Overflow::Auto => force || matches!(computed.scrollbar_gutter, ScrollbarGutter::Stable),
Overflow::Hidden | Overflow::Visible => false,
};
(
reserves(computed.overflow_y, force_y),
reserves(computed.overflow_x, force_x),
)
}
pub(super) fn reserve_scrollbar_gutter(inner: LayoutRect, computed: &ComputedStyle) -> LayoutRect {
reserve_scrollbar_gutter_forced(inner, computed, false, false)
}