mod block;
#[cfg(test)]
mod block_tests;
mod flex;
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);
positioning::place_positioned(self, root_rect);
sticky::place_sticky(self);
positioned_pseudos::place_positioned_pseudos(self, root_rect);
}
}
pub(super) fn layout_node(dom: &mut Dom<TuiExt>, id: NodeId, outer_rect: LayoutRect) {
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()
.cloned()
.unwrap_or_else(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 outer_rect = positioning::apply_relative_shift(&computed, outer_rect, parent_rect);
let inner = compute_content_area_collapsed(
outer_rect,
computed.padding.clone(),
computed.border,
computed.border_collapse,
);
let inner = reserve_scrollbar_gutter(inner, &computed);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.layout = outer_rect;
ext.content_layout = inner;
ext.layout_dirty = false;
}
let measurement = layout_children(dom, id, inner, &computed);
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 let Some(measurement) = measurement
&& matches!(computed.height, crate::layout::Size::Auto)
&& matches!(computed.flow, crate::layout::Flow::Block)
&& parent_is_block_flow
&& !is_out_of_flow_positioned
{
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_cb_w = outer_rect.width;
let pad =
computed.padding.top.resolve(pad_cb_w) + computed.padding.bottom.resolve(pad_cb_w);
let border = computed.border.top.cells() + computed.border.bottom.cells();
let outer_h = content_h.saturating_add(pad).saturating_add(border);
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.layout.height = outer_h;
ext.content_layout.height = content_h;
}
}
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 > inner.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,
);
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 _ = layout_children(dom, id, inner_v2, &computed);
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);
}
}
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;
for child in element_children_of(dom, id) {
if let Some(ext) = dom.node(child).ext() {
let display = ext.computed.as_ref().map(|c| c.display);
let position = ext.computed.as_ref().map(|c| c.position);
if display == Some(crate::layout::Display::None) {
continue;
}
if matches!(
position,
Some(crate::layout::Position::Absolute) | Some(crate::layout::Position::Fixed)
) {
continue;
}
let rect = ext.layout;
let top = rect.y + scroll_y;
let left = rect.x + scroll_x;
let bottom = top + rect.height as i32;
let right = left + rect.width as i32;
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(right);
max_bottom = max_bottom.max(bottom);
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 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 layout_fragment_children(dom: &mut Dom<TuiExt>, id: NodeId, container: LayoutRect) {
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();
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
}
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),
NodeType::Text | NodeType::Comment => {}
}
}
}
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 {
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,
};
let reserve_y = reserves(computed.overflow_y, force_y);
let reserve_x = reserves(computed.overflow_x, 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 reserve_scrollbar_gutter(inner: LayoutRect, computed: &ComputedStyle) -> LayoutRect {
reserve_scrollbar_gutter_forced(inner, computed, false, false)
}