use rdom_core::{Dom, NodeId, NodeType};
use unicode_width::UnicodeWidthStr;
use crate::ext::{PseudoLayout, TuiExt};
use crate::layout::{Display, LayoutRect, Position};
use crate::style::ComputedStyle;
use super::positioning::{
axis_position_anchored, axis_position_relative_shift, axis_size_from_edges, computed_position,
layout_rect, parent_id,
};
pub(super) fn place_positioned_pseudos(dom: &mut Dom<TuiExt>, viewport: LayoutRect) {
if !tree_has_any_positioned_pseudo(dom) {
return;
}
let candidates = collect_hosts(dom, dom.root());
for host in candidates {
if is_display_none_subtree(dom, host) {
continue;
}
place_one(dom, host, viewport);
}
}
fn tree_has_any_positioned_pseudo(dom: &Dom<TuiExt>) -> bool {
fn walk(dom: &Dom<TuiExt>, id: NodeId) -> bool {
for child in dom.node(id).child_nodes() {
let hit = match child.node_type() {
NodeType::Element => child.ext().is_some_and(|e| e.tree_has_positioned_pseudo),
NodeType::Fragment => walk(dom, child.id()),
_ => false,
};
if hit {
return true;
}
}
false
}
walk(dom, dom.root())
}
fn collect_hosts(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 Some(ext) = dom.node(id).ext()
{
let has_positioned_before = ext
.computed_before
.as_ref()
.is_some_and(|c| c.position != Position::Static);
let has_positioned_after = ext
.computed_after
.as_ref()
.is_some_and(|c| c.position != Position::Static);
if has_positioned_before || has_positioned_after {
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 is_display_none_subtree(dom: &Dom<TuiExt>, id: NodeId) -> bool {
let mut cur = Some(id);
while let Some(node) = cur {
if dom
.node(node)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.display == Display::None)
.unwrap_or(false)
{
return true;
}
cur = parent_id(dom, node);
}
false
}
fn place_one(dom: &mut Dom<TuiExt>, host: NodeId, viewport: LayoutRect) {
let (before, after, host_rect) = {
let ext = dom.node(host).ext().expect("host has ext");
(
ext.computed_before.clone(),
ext.computed_after.clone(),
ext.layout,
)
};
if let Some(before_style) = before
&& before_style.position != Position::Static
{
let cb = resolve_containing_block(dom, host, &before_style, viewport, host_rect);
let rect = compute_placed_rect(&before_style, cb, host_rect, PseudoEnd::Before);
if let Some(ext) = dom.node_mut(host).ext_mut() {
ext.before_layout = Some(PseudoLayout {
rect,
position: before_style.position,
});
}
}
if let Some(after_style) = after
&& after_style.position != Position::Static
{
let cb = resolve_containing_block(dom, host, &after_style, viewport, host_rect);
let rect = compute_placed_rect(&after_style, cb, host_rect, PseudoEnd::After);
if let Some(ext) = dom.node_mut(host).ext_mut() {
ext.after_layout = Some(PseudoLayout {
rect,
position: after_style.position,
});
}
}
}
#[derive(Debug, Clone, Copy)]
enum PseudoEnd {
Before,
After,
}
fn resolve_containing_block(
dom: &Dom<TuiExt>,
host: NodeId,
pseudo: &ComputedStyle,
viewport: LayoutRect,
host_rect: LayoutRect,
) -> LayoutRect {
match pseudo.position {
Position::Fixed => viewport,
Position::Absolute => {
let host_position = dom
.node(host)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.position)
.unwrap_or_default();
if matches!(
host_position,
Position::Relative | Position::Absolute | Position::Fixed
) {
return host_rect;
}
let mut cur = parent_id(dom, host);
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);
}
viewport
}
Position::Relative | Position::Static | Position::Sticky => host_rect,
}
}
fn pseudo_content_width(style: &ComputedStyle) -> u16 {
style
.content
.as_deref()
.map(|s| UnicodeWidthStr::width(s) as u16)
.unwrap_or(0)
}
fn pseudo_content_height(style: &ComputedStyle) -> u16 {
style
.content
.as_deref()
.map(|s| s.lines().count().max(1) as u16)
.unwrap_or(1)
}
fn compute_placed_rect(
style: &ComputedStyle,
cb: LayoutRect,
host_rect: LayoutRect,
end: PseudoEnd,
) -> LayoutRect {
let intrinsic_w = pseudo_content_width(style);
let intrinsic_h = pseudo_content_height(style);
let width = axis_size_from_edges(&style.left, &style.right, cb.width, intrinsic_w);
let height = axis_size_from_edges(&style.top, &style.bottom, cb.height, intrinsic_h);
if style.position == Position::Relative {
let (natural_x, natural_y) = match end {
PseudoEnd::Before => (host_rect.x, host_rect.y),
PseudoEnd::After => (
host_rect
.x
.saturating_add(host_rect.width as i32)
.saturating_sub(intrinsic_w as i32),
host_rect.y,
),
};
let x = axis_position_relative_shift(&style.left, &style.right, natural_x, cb.width as i32);
let y =
axis_position_relative_shift(&style.top, &style.bottom, natural_y, cb.height as i32);
LayoutRect::new(x, y, width, height)
} else {
let x = axis_position_anchored(&style.left, &style.right, cb.x, cb.width, width);
let y = axis_position_anchored(&style.top, &style.bottom, cb.y, cb.height, height);
LayoutRect::new(x, y, width, height)
}
}