use rdom_core::{NodeId, NodeType};
use unicode_width::UnicodeWidthStr;
use crate::TuiDom;
use crate::layout::Size;
use crate::style::Value;
const CELL_H_PADDING: u16 = 2;
pub fn size_all_tables(dom: &mut TuiDom) {
let tables = collect_tables(dom, dom.root());
for table in tables {
size_columns(dom, table);
}
}
pub fn size_columns(dom: &mut TuiDom, table: NodeId) {
let rows = collect_rows(dom, table);
if rows.is_empty() {
return;
}
let mut explicit: Vec<Option<u16>> = Vec::new();
let mut content: Vec<u16> = Vec::new();
let mut spans: Vec<Span> = Vec::new();
for &row_id in &rows {
let mut slot = 0usize;
for cell in collect_cells(dom, row_id) {
let len = colspan_of(dom, cell);
while content.len() < slot + len {
explicit.push(None);
content.push(0);
}
let author = cell_author_width(dom, cell);
let total = text_content_width(dom, cell).saturating_add(CELL_H_PADDING);
if len == 1 {
if let Some(w) = author {
explicit[slot] = Some(explicit[slot].map_or(w, |e| e.max(w)));
}
content[slot] = content[slot].max(total);
} else {
spans.push(Span {
start: slot,
len,
want: author.unwrap_or(total),
});
}
slot += len;
}
}
let mut used: Vec<u16> = content
.iter()
.enumerate()
.map(|(i, &c)| explicit[i].unwrap_or(c))
.collect();
for span in &spans {
let cols = &mut used[span.start..span.start + span.len];
let sum: u32 = cols.iter().map(|&w| u32::from(w)).sum();
let deficit = u32::from(span.want).saturating_sub(sum);
if deficit == 0 {
continue;
}
let each = (deficit / span.len as u32).min(u32::from(u16::MAX)) as u16;
let extra = (deficit % span.len as u32) as usize;
for (i, w) in cols.iter_mut().enumerate() {
*w = w.saturating_add(each).saturating_add(u16::from(i < extra));
}
}
for &row_id in &rows {
let mut slot = 0usize;
for cell in collect_cells(dom, row_id) {
let len = colspan_of(dom, cell);
let w = used[slot..slot + len]
.iter()
.fold(0u16, |acc, &c| acc.saturating_add(c));
if let Some(ext) = dom.node_mut(cell).ext_mut() {
ext.table_used_width = Some(w);
}
slot += len;
}
}
}
struct Span {
start: usize,
len: usize,
want: u16,
}
fn colspan_of(dom: &TuiDom, cell: NodeId) -> usize {
dom.node(cell)
.get_attribute("colspan")
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|&n| n >= 1)
.map_or(1, |n| n.min(1000))
}
fn cell_author_width(dom: &TuiDom, cell: NodeId) -> Option<u16> {
match dom.node(cell).ext()?.inline_style.as_deref()?.width {
Some(Value::Specified(Size::Fixed(w))) => Some(w),
_ => None,
}
}
fn collect_tables(dom: &TuiDom, root: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
walk_tables(dom, root, &mut out);
out
}
fn walk_tables(dom: &TuiDom, id: NodeId, out: &mut Vec<NodeId>) {
if dom.node(id).tag_name() == Some("table") {
out.push(id);
}
for child in dom.node(id).child_nodes() {
walk_tables(dom, child.id(), out);
}
}
fn collect_rows(dom: &TuiDom, table: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
for child in dom.node(table).child_nodes() {
match child.tag_name() {
Some("tr") => out.push(child.id()),
Some("thead") | Some("tbody") | Some("tfoot") => {
for grand in child.child_nodes() {
if grand.tag_name() == Some("tr") {
out.push(grand.id());
}
}
}
_ => {}
}
}
out
}
fn collect_cells(dom: &TuiDom, row: NodeId) -> Vec<NodeId> {
dom.node(row)
.child_nodes()
.filter(|c| matches!(c.tag_name(), Some("td") | Some("th")))
.map(|c| c.id())
.collect()
}
fn text_content_width(dom: &TuiDom, cell: NodeId) -> u16 {
let mut text = String::new();
collect_text(dom, cell, &mut text);
UnicodeWidthStr::width(text.as_str()) as u16
}
fn collect_text(dom: &TuiDom, id: NodeId, out: &mut String) {
for child in dom.node(id).child_nodes() {
if child.node_type() == NodeType::Text {
if let Some(s) = child.node_value() {
out.push_str(s);
}
} else {
collect_text(dom, child.id(), out);
}
}
}
#[cfg(test)]
mod tests;