use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::{LayoutRect, Length, Position, Size};
use crate::node::TuiNodeExt;
use crate::style::ComputedStyle;
pub(crate) fn containing_block(dom: &Dom<TuiExt>, id: NodeId, viewport: LayoutRect) -> LayoutRect {
let position = computed_position(dom, id);
if position == Position::Fixed {
return viewport;
}
if position == Position::Absolute {
let mut cur = parent_id(dom, id);
while let Some(p) = cur {
let pp = computed_position(dom, p);
if matches!(
pp,
Position::Relative | Position::Absolute | Position::Fixed
) {
return layout_rect(dom, p).unwrap_or(viewport);
}
cur = parent_id(dom, p);
}
return viewport;
}
parent_id(dom, id)
.and_then(|p| layout_rect(dom, p))
.unwrap_or(viewport)
}
pub(super) fn computed_position(dom: &Dom<TuiExt>, id: NodeId) -> Position {
dom.node(id)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.position)
.unwrap_or_default()
}
pub(super) fn layout_rect(dom: &Dom<TuiExt>, id: NodeId) -> Option<LayoutRect> {
dom.node(id).ext().map(|e| e.layout)
}
pub(super) fn parent_id(dom: &Dom<TuiExt>, id: NodeId) -> Option<NodeId> {
dom.node(id).parent_node().map(|p| p.id())
}
pub(super) fn apply_relative_shift(
computed: &ComputedStyle,
rect: LayoutRect,
parent: LayoutRect,
) -> LayoutRect {
if computed.position != Position::Relative {
return rect;
}
let dx =
resolve_length_offset(&computed.left, parent.width as i32, false).unwrap_or_else(|| {
resolve_length_offset(&computed.right, parent.width as i32, true).unwrap_or(0)
});
let dy =
resolve_length_offset(&computed.top, parent.height as i32, false).unwrap_or_else(|| {
resolve_length_offset(&computed.bottom, parent.height as i32, true).unwrap_or(0)
});
LayoutRect::new(
rect.x.saturating_add(dx),
rect.y.saturating_add(dy),
rect.width,
rect.height,
)
}
fn resolve_length_offset(len: &Length, basis: i32, negate: bool) -> Option<i32> {
let cells = match len {
Length::Auto => return None,
Length::Cells(n) => *n as i32,
Length::Calc(expr) => expr.resolve(&rdom_style::calc::ResolveCtx::new(basis)),
};
Some(if negate { -cells } else { cells })
}
pub(super) fn place_positioned(dom: &mut Dom<TuiExt>, viewport: LayoutRect) {
let positioned = collect_positioned(dom, dom.root());
for id in positioned {
let cb = containing_block(dom, id, viewport);
let computed = dom
.node(id)
.computed()
.cloned()
.unwrap_or_else(ComputedStyle::initial);
let placed = compute_placed_rect(&computed, cb);
super::layout_node(dom, id, placed);
}
}
fn collect_positioned(dom: &Dom<TuiExt>, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
walk_for_positioned(dom, id, &mut out);
out
}
fn walk_for_positioned(dom: &Dom<TuiExt>, id: NodeId, out: &mut Vec<NodeId>) {
if dom.node(id).node_type() == NodeType::Element {
let pos = computed_position(dom, id);
if matches!(pos, Position::Absolute | Position::Fixed) {
out.push(id);
}
}
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element | NodeType::Fragment => {
walk_for_positioned(dom, child.id(), out);
}
_ => {}
}
}
}
fn compute_placed_rect(c: &ComputedStyle, cb: LayoutRect) -> LayoutRect {
let width = resolve_size_axis(&c.width, cb.width, &c.left, &c.right, cb.width);
let height = resolve_size_axis(&c.height, cb.height, &c.top, &c.bottom, cb.height);
use crate::layout::MarginValue;
let (cx_left, cx_right) = (c.margin.left.clone(), c.margin.right.clone());
let (cy_top, cy_bottom) = (c.margin.top.clone(), c.margin.bottom.clone());
let margin_cb_w = cb.width;
let basis_w = cb.width as i32;
let basis_h = cb.height as i32;
let x = if length_to_cells_opt(&c.left, basis_w).is_some()
&& length_to_cells_opt(&c.right, basis_w).is_some()
&& matches!(cx_left, MarginValue::Auto)
&& matches!(cx_right, MarginValue::Auto)
{
let left = length_to_cells_opt(&c.left, basis_w).unwrap_or(0);
let right = length_to_cells_opt(&c.right, basis_w).unwrap_or(0);
let span = basis_w.saturating_sub(left + right);
let extra = span.saturating_sub(width as i32).max(0);
cb.x + left + extra / 2
} else {
let base = axis_position_anchored(&c.left, &c.right, cb.x, cb.width, width);
let start_margin = match &cx_left {
MarginValue::Cells(n) => *n as i32,
MarginValue::Auto => 0,
MarginValue::Calc(_) => cx_left.resolve(margin_cb_w) as i32,
};
base + start_margin
};
let y = if length_to_cells_opt(&c.top, basis_h).is_some()
&& length_to_cells_opt(&c.bottom, basis_h).is_some()
&& matches!(cy_top, MarginValue::Auto)
&& matches!(cy_bottom, MarginValue::Auto)
{
let top = length_to_cells_opt(&c.top, basis_h).unwrap_or(0);
let bottom = length_to_cells_opt(&c.bottom, basis_h).unwrap_or(0);
let span = basis_h.saturating_sub(top + bottom);
let extra = span.saturating_sub(height as i32).max(0);
cb.y + top + extra / 2
} else {
let base = axis_position_anchored(&c.top, &c.bottom, cb.y, cb.height, height);
let start_margin = match &cy_top {
MarginValue::Cells(n) => *n as i32,
MarginValue::Auto => 0,
MarginValue::Calc(_) => cy_top.resolve(margin_cb_w) as i32,
};
base + start_margin
};
LayoutRect::new(x, y, width, height)
}
fn resolve_size_axis(
size: &Size,
cb_extent: u16,
start: &Length,
end: &Length,
edges_basis: u16,
) -> u16 {
match size {
Size::Fixed(n) => *n,
Size::Flex(_) => cb_extent,
Size::Percent(p) => ((cb_extent 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(cb_extent as i32));
v.max(0).min(u16::MAX as i32) as u16
}
Size::Auto => axis_size_from_edges(start, end, edges_basis, 0),
}
}
fn length_to_cells_opt(len: &Length, basis: i32) -> Option<i32> {
length_to_cells(len, basis)
}
pub(super) fn axis_size_from_edges(
start: &Length,
end: &Length,
cb_extent: u16,
fallback: u16,
) -> u16 {
let basis = cb_extent as i32;
let s = length_to_cells(start, basis);
let e = length_to_cells(end, basis);
match (s, e) {
(Some(s), Some(e)) => {
let span = s.saturating_add(e);
(basis.saturating_sub(span)).max(0) as u16
}
_ => fallback,
}
}
fn length_to_cells(len: &Length, basis: i32) -> Option<i32> {
match len {
Length::Auto => None,
Length::Cells(n) => Some(*n as i32),
Length::Calc(expr) => Some(expr.resolve(&rdom_style::calc::ResolveCtx::new(basis))),
}
}
pub(super) fn axis_position_anchored(
start: &Length,
end: &Length,
cb_start: i32,
cb_extent: u16,
size: u16,
) -> i32 {
let basis = cb_extent as i32;
let s = length_to_cells(start, basis);
let e = length_to_cells(end, basis);
match (s, e) {
(Some(s), _) => cb_start.saturating_add(s),
(None, Some(e)) => cb_start
.saturating_add(basis)
.saturating_sub(e)
.saturating_sub(size as i32),
_ => cb_start,
}
}
pub(super) fn axis_position_relative_shift(
start: &Length,
end: &Length,
anchor: i32,
basis: i32,
) -> i32 {
let s = length_to_cells(start, basis);
let e = length_to_cells(end, basis);
match (s, e) {
(Some(s), _) => anchor.saturating_add(s),
(None, Some(e)) => anchor.saturating_sub(e),
_ => anchor,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::{Length, ZIndex};
use crate::render::rect::Rect;
use crate::style::Value;
use crate::style::{ComputedStyle, Stylesheet, TuiStyle};
use crate::{CascadeExt, LayoutExt, TuiDom};
fn build_dom_with_positioned_chain(positions: &[Position]) -> (TuiDom, Vec<NodeId>) {
let mut dom: TuiDom = TuiDom::new();
let mut ids = Vec::with_capacity(positions.len());
let root = dom.root();
let mut parent = root;
for (i, _p) in positions.iter().enumerate() {
let id = dom.create_element("div");
dom.node_mut(id).set_id(&format!("n{i}")).unwrap();
dom.append_child(parent, id).unwrap();
ids.push(id);
parent = id;
}
let mut sheet = Stylesheet::bare();
for (i, p) in positions.iter().enumerate() {
sheet = sheet.rule_unchecked(&format!("#n{i}"), TuiStyle::new().position(*p));
}
dom.cascade(&sheet);
let viewport = Rect::new(0, 0, 100, 50);
dom.layout_dom(viewport);
(dom, ids)
}
fn viewport() -> LayoutRect {
LayoutRect::new(0, 0, 100, 50)
}
#[test]
fn absolute_with_no_positioned_ancestor_returns_viewport() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Static,
Position::Static,
Position::Absolute,
]);
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, viewport());
}
#[test]
fn absolute_inside_relative_uses_relative_parent() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Static,
Position::Relative,
Position::Absolute,
]);
let parent_rect = dom.node(ids[1]).ext().unwrap().layout;
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, parent_rect);
}
#[test]
fn absolute_skips_static_ancestors_to_find_relative() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Relative, Position::Static, Position::Absolute, ]);
let grandparent_rect = dom.node(ids[0]).ext().unwrap().layout;
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, grandparent_rect);
}
#[test]
fn absolute_inside_absolute_uses_absolute_parent() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Static,
Position::Absolute,
Position::Absolute,
]);
let parent_rect = dom.node(ids[1]).ext().unwrap().layout;
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, parent_rect);
}
#[test]
fn fixed_always_uses_viewport_even_with_relative_ancestor() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Static,
Position::Relative,
Position::Fixed,
]);
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, viewport());
}
#[test]
fn fixed_uses_viewport_when_no_ancestors_positioned() {
let (dom, ids) =
build_dom_with_positioned_chain(&[Position::Static, Position::Static, Position::Fixed]);
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, viewport());
}
#[test]
fn static_returns_parent_layout() {
let (dom, ids) = build_dom_with_positioned_chain(&[
Position::Static,
Position::Static,
Position::Static,
]);
let parent_rect = dom.node(ids[1]).ext().unwrap().layout;
let cb = containing_block(&dom, ids[2], viewport());
assert_eq!(cb, parent_rect);
}
#[allow(dead_code)]
fn _types_compile() {
let _: ComputedStyle = ComputedStyle::initial();
let _: Value<Length> = Value::Specified(Length::Auto);
let _: ZIndex = ZIndex::Auto;
}
}