use rdom_core::{Dom, NodeId};
use crate::ext::TuiExt;
use crate::layout::{CornerStyle, Display, Overflow};
use crate::node::TuiNodeExt;
use crate::render::buffer::{BorderContribution, BorderSide, DIR_E, DIR_N, DIR_S};
use crate::render::{Buffer, Rect, Style};
use crate::style::Color;
use rdom_style::layout::BorderStyle;
const TREE_INDENT: i32 = 2;
pub(super) fn paint_tree_guides(dom: &Dom<TuiExt>, buf: &mut Buffer, clip: Rect) {
let mut trees = Vec::new();
collect_trees(dom, dom.root(), &mut trees);
for tree in trees {
let tree_clip = clip_for_tree(dom, tree, clip);
let span = tree_padding_box(dom, tree)
.map(|r| {
let gutter = if reserves_vscrollbar(dom, tree) { 1 } else { 0 };
(r.x, r.x + r.width as i32 - gutter)
})
.unwrap_or((tree_clip.x as i32, tree_clip.right() as i32));
for item in treeitem_children(dom, tree) {
paint_item(dom, item, buf, tree_clip, &[], span);
}
}
}
fn clip_for_tree(dom: &Dom<TuiExt>, tree: NodeId, base_clip: Rect) -> Rect {
use crate::layout::Overflow;
let mut clip = base_clip;
let mut cur = Some(tree);
while let Some(id) = cur {
if let Some(computed) = dom.node(id).ext().and_then(|e| e.computed.as_ref()) {
let clips = !matches!(computed.overflow_x, Overflow::Visible)
|| !matches!(computed.overflow_y, Overflow::Visible);
if clips && let Some(outer) = dom.node(id).layout_rect() {
let padding_box = crate::layout::compute_padding_box(outer, computed.border);
clip = match super::layout_rect_to_grid(padding_box, clip) {
Some(grid) => clip.intersection(grid),
None => return Rect::new(clip.x, clip.y, 0, 0),
};
}
}
cur = dom.node(id).parent_node().map(|p| p.id());
}
clip
}
fn tree_padding_box(dom: &Dom<TuiExt>, tree: NodeId) -> Option<crate::layout::LayoutRect> {
let outer = dom.node(tree).layout_rect()?;
let border = dom
.node(tree)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| c.border)
.unwrap_or_default();
Some(crate::layout::compute_padding_box(outer, border))
}
fn reserves_vscrollbar(dom: &Dom<TuiExt>, tree: NodeId) -> bool {
let Some(ext) = dom.node(tree).ext() else {
return false;
};
let Some(c) = ext.computed.as_ref() else {
return false;
};
matches!(c.overflow_y, Overflow::Scroll | Overflow::Auto)
&& super::scrollbar::should_paint(
c.overflow_y,
ext.content_layout.height as usize,
ext.scroll_content_height,
)
}
fn collect_trees(dom: &Dom<TuiExt>, id: NodeId, out: &mut Vec<NodeId>) {
if role(dom, id) == Some("tree") {
out.push(id);
}
for child in dom.node(id).child_nodes() {
collect_trees(dom, child.id(), out);
}
}
fn paint_item(
dom: &Dom<TuiExt>,
item: NodeId,
buf: &mut Buffer,
clip: Rect,
trunks: &[(u16, bool)],
span: (i32, i32),
) {
if is_hidden(dom, item) {
return;
}
let Some(rect) = dom.node(item).layout_rect() else {
return;
};
let row_y = rect.y;
let color = guide_color(dom, item);
let label_height = match child_group(dom, item) {
Some(g) if !is_hidden(dom, g) => dom
.node(g)
.layout_rect()
.map(|gr| (gr.y - rect.y).max(1))
.unwrap_or(rect.height as i32),
_ => (rect.height as i32).max(1),
};
if let Some(bg) = row_highlight(dom, item) {
for dy in 0..label_height {
fill_row_bg(buf, clip, span, row_y + dy, bg);
}
}
if let Some(expanded) = dom.node(item).get_attribute("aria-expanded") {
let glyph = if expanded == "true" { "▼" } else { "▶" };
put_glyph(buf, clip, rect.x, row_y, glyph, color);
}
for &(col, continues) in trunks {
if continues {
for dy in 0..label_height {
put(buf, clip, col as i32, row_y + dy, &[DIR_N, DIR_S], color);
}
}
}
let parent = dom.node(item).parent_node().map(|p| p.id());
let in_group = parent
.map(|p| role(dom, p) == Some("group"))
.unwrap_or(false);
let own_col = rect.x - TREE_INDENT;
let mut is_last = false;
if in_group && let Some(p) = parent {
is_last = treeitem_children(dom, p).last() == Some(&item);
let connector: &[usize] = if is_last {
&[DIR_N, DIR_E]
} else {
&[DIR_N, DIR_E, DIR_S]
};
put(buf, clip, own_col, row_y, connector, color);
if !is_last {
for dy in 1..label_height {
put(buf, clip, own_col, row_y + dy, &[DIR_N, DIR_S], color);
}
}
}
if let Some(group) = child_group(dom, item)
&& !is_hidden(dom, group)
{
let mut child_trunks = trunks.to_vec();
if in_group && own_col >= 0 {
child_trunks.push((own_col as u16, !is_last));
}
for child in treeitem_children(dom, group) {
paint_item(dom, child, buf, clip, &child_trunks, span);
}
}
}
fn put(buf: &mut Buffer, clip: Rect, x: i32, y: i32, dirs: &[usize], color: Color) {
if x < 0 || y < 0 {
return;
}
let (xu, yu) = (x as u16, y as u16);
if !clip.contains(xu, yu) {
return;
}
for &dir in dirs {
buf.add_border_dir(
xu,
yu,
dir,
BorderContribution {
style: BorderStyle::Solid,
fg: color,
priority: 0,
corner_style: CornerStyle::Square,
side: BorderSide::Top,
},
);
}
}
fn row_highlight(dom: &Dom<TuiExt>, item: NodeId) -> Option<Color> {
let bg = dom.node(item).computed().map(|c| c.bg)?;
(bg != Color::Reset).then_some(bg)
}
fn fill_row_bg(buf: &mut Buffer, clip: Rect, span: (i32, i32), y: i32, bg: Color) {
if y < 0 {
return;
}
let yu = y as u16;
if yu < clip.y || yu >= clip.bottom() {
return;
}
let x0 = span.0.max(clip.x as i32).max(0) as u16;
let x1 = span.1.min(clip.right() as i32).max(0) as u16;
for x in x0..x1 {
if let Some(cell) = buf.cell_mut(x, yu) {
cell.set_bg(bg);
}
}
}
fn put_glyph(buf: &mut Buffer, clip: Rect, x: i32, y: i32, glyph: &str, fg: Color) {
if x < 0 || y < 0 {
return;
}
let (xu, yu) = (x as u16, y as u16);
if !clip.contains(xu, yu) {
return;
}
buf.set_stringn(xu, yu, glyph, 1, Style::new().fg(fg));
}
fn role(dom: &Dom<TuiExt>, id: NodeId) -> Option<&str> {
dom.node(id).get_attribute("role")
}
fn is_hidden(dom: &Dom<TuiExt>, id: NodeId) -> bool {
dom.node(id)
.computed()
.map(|c| c.display == Display::None)
.unwrap_or(false)
}
fn guide_color(dom: &Dom<TuiExt>, id: NodeId) -> Color {
dom.node(id)
.computed()
.map(|c| c.border_fg)
.unwrap_or(Color::Reset)
}
fn treeitem_children(dom: &Dom<TuiExt>, container: NodeId) -> Vec<NodeId> {
dom.node(container)
.child_nodes()
.filter(|n| n.get_attribute("role") == Some("treeitem"))
.map(|n| n.id())
.collect()
}
fn child_group(dom: &Dom<TuiExt>, item: NodeId) -> Option<NodeId> {
dom.node(item)
.child_nodes()
.find(|n| n.get_attribute("role") == Some("group"))
.map(|n| n.id())
}