use rdom_core::{Dom, NodeId, NodeType};
use crate::ext::TuiExt;
use crate::layout::{LayoutRect, Length, Overflow, Position};
use crate::node::TuiNodeExt;
pub(super) fn place_sticky(dom: &mut Dom<TuiExt>) {
let sticky_ids = collect_sticky(dom, dom.root());
for id in sticky_ids {
place_one(dom, id);
}
}
fn collect_sticky(dom: &Dom<TuiExt>, id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
walk(dom, id, &mut out);
out
}
fn walk(dom: &Dom<TuiExt>, id: NodeId, out: &mut Vec<NodeId>) {
if dom.node(id).node_type() == NodeType::Element {
let pos = dom
.node(id)
.computed()
.map(|c| c.position)
.unwrap_or(Position::Static);
if pos == Position::Sticky {
out.push(id);
}
}
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Element | NodeType::Fragment => walk(dom, child.id(), out),
_ => {}
}
}
}
fn place_one(dom: &mut Dom<TuiExt>, id: NodeId) {
let Some(natural) = dom.node(id).ext().map(|e| e.layout) else {
return;
};
let computed = match dom.node(id).computed().cloned() {
Some(c) => c,
None => return,
};
let (top_inset, left_inset) = (computed.top, computed.left);
let scrollport = nearest_scrollport(dom, id);
let Some((scrollport_id, scrollport_rect)) = scrollport else {
return;
};
let cb_rect = dom
.node(id)
.parent_node()
.and_then(|p| p.ext().map(|e| e.content_layout))
.unwrap_or(scrollport_rect);
let mut placed = natural;
if let Length::Cells(n) = top_inset {
let pin_y = scrollport_rect.y.saturating_add(n as i32);
if placed.y < pin_y {
placed.y = pin_y;
}
let cb_far = cb_rect.bottom().saturating_sub(placed.height as i32);
if placed.y > cb_far {
placed.y = cb_far;
}
if placed.y < natural.y && natural.y < pin_y {
placed.y = natural.y;
}
}
if let Length::Cells(n) = left_inset {
let pin_x = scrollport_rect.x.saturating_add(n as i32);
if placed.x < pin_x {
placed.x = pin_x;
}
let cb_far = cb_rect.right().saturating_sub(placed.width as i32);
if placed.x > cb_far {
placed.x = cb_far;
}
}
if placed == natural {
return;
}
let dx = placed.x - natural.x;
let dy = placed.y - natural.y;
let _ = scrollport_id; shift_subtree(dom, id, dx, dy);
}
fn shift_subtree(dom: &mut Dom<TuiExt>, id: NodeId, dx: i32, dy: i32) {
if let Some(ext) = dom.node_mut(id).ext_mut() {
ext.layout = LayoutRect::new(
ext.layout.x + dx,
ext.layout.y + dy,
ext.layout.width,
ext.layout.height,
);
ext.content_layout = LayoutRect::new(
ext.content_layout.x + dx,
ext.content_layout.y + dy,
ext.content_layout.width,
ext.content_layout.height,
);
for anon in &mut ext.anonymous_blocks {
anon.rect = LayoutRect::new(
anon.rect.x + dx,
anon.rect.y + dy,
anon.rect.width,
anon.rect.height,
);
}
for pseudo in [&mut ext.before_layout, &mut ext.after_layout]
.into_iter()
.flatten()
{
pseudo.rect = LayoutRect::new(
pseudo.rect.x + dx,
pseudo.rect.y + dy,
pseudo.rect.width,
pseudo.rect.height,
);
}
}
let child_ids: Vec<NodeId> = dom
.node(id)
.child_nodes()
.filter(|c| {
matches!(
c.node_type(),
NodeType::Element | NodeType::Fragment | NodeType::Text
)
})
.map(|c| c.id())
.collect();
for c in child_ids {
shift_subtree(dom, c, dx, dy);
}
}
fn nearest_scrollport(dom: &Dom<TuiExt>, id: NodeId) -> Option<(NodeId, LayoutRect)> {
let mut cursor = dom.node(id).parent_node();
while let Some(p) = cursor {
if p.node_type() == NodeType::Element {
let computed = p.computed();
let scrollable = computed
.map(|c| c.overflow_x != Overflow::Visible || c.overflow_y != Overflow::Visible)
.unwrap_or(false);
if scrollable && let Some(ext) = p.ext() {
let border = computed.map(|c| c.border).unwrap_or_default();
let scrollport = rdom_style::layout::compute_padding_box(ext.layout, border);
return Some((p.id(), scrollport));
}
}
cursor = p.parent_node();
}
None
}