use rdom_core::{Dom, NodeId, NodeType};
use unicode_width::UnicodeWidthStr;
use crate::ext::TuiExt;
use crate::layout::{Direction, Size};
use crate::node::TuiNodeExt;
use crate::render::inline::compute_inline_layout;
use crate::style::ComputedStyle;
use super::ifc::is_ifc_block;
pub(crate) fn intrinsic_size(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
cross_budget: u16,
containing_block_width: u16,
) -> u16 {
intrinsic_size_inner(
dom,
id,
direction,
cross_budget,
containing_block_width,
IntrinsicMode::BoxSize,
Measure::MaxContent,
)
}
pub(super) fn content_min_size(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
cross_budget: u16,
containing_block_width: u16,
) -> u16 {
intrinsic_size_inner(
dom,
id,
direction,
cross_budget,
containing_block_width,
IntrinsicMode::ContentOnly,
Measure::MinContent,
)
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) enum Measure {
MaxContent,
MinContent,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub(super) enum IntrinsicMode {
BoxSize,
ContentOnly,
}
fn intrinsic_size_inner(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
cross_budget: u16,
containing_block_width: u16,
mode: IntrinsicMode,
measure: Measure,
) -> u16 {
let kind = dom.node(id).node_type();
match kind {
NodeType::Text => intrinsic_text(dom, id, direction),
NodeType::Element | NodeType::Fragment => intrinsic_element(
dom,
id,
direction,
cross_budget,
containing_block_width,
mode,
measure,
),
_ => 0,
}
}
fn intrinsic_text(dom: &Dom<TuiExt>, id: NodeId, direction: Direction) -> u16 {
let text = dom.text_content(id);
match direction {
Direction::Row => {
text.lines()
.map(|line| UnicodeWidthStr::width(line) as u16)
.max()
.unwrap_or(0)
}
Direction::Column => text.lines().count().max(1) as u16,
}
}
fn intrinsic_element(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
cross_budget: u16,
containing_block_width: u16,
mode: IntrinsicMode,
measure: Measure,
) -> u16 {
let computed = dom
.node(id)
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
if mode == IntrinsicMode::BoxSize {
if direction == Direction::Row
&& let Some(w) = dom.node(id).ext().and_then(|e| e.table_used_width)
{
return w;
}
let declared = match direction {
Direction::Row => &computed.width,
Direction::Column => &computed.height,
};
if let Size::Fixed(n) = declared {
return *n;
}
}
let cb_w_for_pad = containing_block_width;
let pad_main = match direction {
Direction::Row => {
computed.padding.left.resolve(cb_w_for_pad)
+ computed.padding.right.resolve(cb_w_for_pad)
}
Direction::Column => {
computed.padding.top.resolve(cb_w_for_pad)
+ computed.padding.bottom.resolve(cb_w_for_pad)
}
};
let border_main = border_main_cost(&computed, direction);
let (gutter_col, gutter_row) = super::gutter_axes(&computed, false, false);
let gutter_main = match direction {
Direction::Row => u16::from(gutter_col),
Direction::Column => u16::from(gutter_row),
};
let border_main = border_main.saturating_add(gutter_main);
let pseudo_main = match direction {
Direction::Row => pseudo_content_width(dom, id),
Direction::Column => 0,
};
let pseudo_beside_inline = match measure {
Measure::MinContent => 0,
Measure::MaxContent => pseudo_main,
};
if is_ifc_block(dom, id) {
let content = match direction {
Direction::Row => inline_width(dom, id, measure),
Direction::Column => {
let outer_width = match &computed.width {
Size::Fixed(n) => *n,
_ => cross_budget,
};
let row_pad = computed.padding.left.resolve(outer_width)
+ computed.padding.right.resolve(outer_width);
let row_border = border_main_cost(&computed, Direction::Row);
let content_width = outer_width
.saturating_sub(row_pad)
.saturating_sub(row_border);
let layout = compute_inline_layout(dom, id, content_width);
layout.height().max(1)
}
};
return content
.saturating_add(pseudo_beside_inline)
.saturating_add(pad_main)
.saturating_add(border_main);
}
let children: Vec<NodeId> = super::element_children_of(dom, id)
.into_iter()
.filter(|&c| super::is_in_flow(dom, c))
.collect();
if children.is_empty() {
if has_non_whitespace_text(dom, id) {
let content = match direction {
Direction::Row => inline_width(dom, id, measure),
Direction::Column => {
let outer_width = match &computed.width {
Size::Fixed(n) => *n,
_ => cross_budget,
};
let row_pad = computed.padding.left.resolve(outer_width)
+ computed.padding.right.resolve(outer_width);
let row_border = border_main_cost(&computed, Direction::Row);
let content_width = outer_width
.saturating_sub(row_pad)
.saturating_sub(row_border);
let layout = compute_inline_layout(dom, id, content_width);
layout.height().max(1)
}
};
return content
.saturating_add(pseudo_beside_inline)
.saturating_add(pad_main)
.saturating_add(border_main);
}
return pseudo_main
.saturating_add(pad_main)
.saturating_add(border_main);
}
let child_cross_budget = match direction {
Direction::Row => cross_budget.saturating_sub(
(computed.padding.top.resolve(cross_budget)
+ computed.padding.bottom.resolve(cross_budget))
.saturating_add(border_main_cost(&computed, Direction::Column)),
),
Direction::Column => cross_budget.saturating_sub(
(computed.padding.left.resolve(cross_budget)
+ computed.padding.right.resolve(cross_budget))
.saturating_add(border_main_cost(&computed, Direction::Row)),
),
};
let child_cb_width = match direction {
Direction::Row => 0,
Direction::Column => child_cross_budget,
};
let outer = |c: NodeId| {
let inner = intrinsic_size_inner(
dom,
c,
direction,
child_cross_budget,
child_cb_width,
IntrinsicMode::BoxSize,
measure,
);
let margins = dom
.node(c)
.ext()
.and_then(|e| e.computed.as_ref())
.map(|cs| {
let (a, b) = match direction {
Direction::Row => (&cs.margin.left, &cs.margin.right),
Direction::Column => (&cs.margin.top, &cs.margin.bottom),
};
i32::from(a.resolve(child_cb_width)) + i32::from(b.resolve(child_cb_width))
})
.unwrap_or(0);
(i32::from(inner) + margins).clamp(0, i32::from(u16::MAX)) as u16
};
let intrinsic_children: u16 = if computed.direction == direction {
let gap_total = computed
.gap
.resolve(0)
.saturating_mul((children.len() as u16).saturating_sub(1));
let children_main: u16 = children
.iter()
.map(|&c| outer(c))
.fold(0u16, |acc, n| acc.saturating_add(n));
children_main.saturating_add(gap_total)
} else {
children.iter().map(|&c| outer(c)).max().unwrap_or(0)
};
let text_runs = dom
.node(id)
.child_nodes()
.filter(|c| {
c.node_type() == NodeType::Text
&& c.node_value()
.is_some_and(|t| !t.chars().all(char::is_whitespace))
})
.map(|c| intrinsic_text(dom, c.id(), direction));
let with_text = match direction {
Direction::Column => text_runs.fold(intrinsic_children, |acc, n| acc.saturating_add(n)),
Direction::Row => text_runs.fold(intrinsic_children, |acc, n| acc.max(n)),
};
let with_text = match direction {
Direction::Column => {
with_text.saturating_add(own_line_pseudo_rows(dom, id, child_cross_budget))
}
Direction::Row => with_text,
};
with_text
.saturating_add(pseudo_main)
.saturating_add(pad_main)
.saturating_add(border_main)
}
fn pseudo_content_width(dom: &Dom<TuiExt>, id: NodeId) -> u16 {
use crate::ext::StyleSlot;
use crate::render::inline::generated;
let width = |host: NodeId, slot: StyleSlot| -> u32 {
let node = dom.node(host);
let computed = match slot {
StyleSlot::Before => node.computed_before(),
_ => node.computed_after(),
};
computed
.and_then(|c| c.content.as_deref())
.map_or(0, |t| UnicodeWidthStr::width(t) as u32)
};
let mut acc: u32 = 0;
for item in generated::deferred_markers(dom, id) {
acc = acc.saturating_add(width(item, StyleSlot::Before));
}
if generated::marker_line_holder(dom, id).is_none() {
acc = acc.saturating_add(width(id, StyleSlot::Before));
}
acc = acc.saturating_add(width(id, StyleSlot::After));
acc.min(u16::MAX as u32) as u16
}
fn own_line_pseudo_rows(dom: &Dom<TuiExt>, id: NodeId, content_width: u16) -> u16 {
use crate::render::inline::{RunPseudos, generated, pack_run};
let own_line = generated::own_line_pseudos(dom, id);
let rows = |pseudos: RunPseudos| pack_run(dom, id, &[], pseudos, content_width).height();
let mut total = 0u16;
if own_line.before {
total = total.saturating_add(rows(RunPseudos {
before: true,
after: false,
}));
}
if own_line.after {
total = total.saturating_add(rows(RunPseudos {
before: false,
after: true,
}));
}
total
}
pub(super) fn has_non_whitespace_text(dom: &Dom<TuiExt>, id: NodeId) -> bool {
for child in dom.node(id).child_nodes() {
if child.node_type() == NodeType::Text
&& let Some(text) = child.node_value()
&& !text.chars().all(char::is_whitespace)
{
return true;
}
}
false
}
pub(super) fn border_main_cost(computed: &ComputedStyle, direction: Direction) -> u16 {
let b = computed.border;
match direction {
Direction::Row => b.left.cells() + b.right.cells(),
Direction::Column => b.top.cells() + b.bottom.cells(),
}
}
fn inline_width(dom: &Dom<TuiExt>, id: NodeId, measure: Measure) -> u16 {
match measure {
Measure::MaxContent => inline_content_width(dom, id),
Measure::MinContent => min_content_inline_width(dom, id),
}
}
fn min_content_inline_width(dom: &Dom<TuiExt>, id: NodeId) -> u16 {
compute_inline_layout(dom, id, 0)
.lines
.iter()
.map(|line| line.width)
.max()
.unwrap_or(0)
}
pub(super) fn inline_content_width(dom: &Dom<TuiExt>, id: NodeId) -> u16 {
fn walk(dom: &Dom<TuiExt>, id: NodeId, acc: &mut u32) {
use crate::ext::StyleSlot;
use crate::layout::{Display, Position};
use crate::render::inline::generated;
for child in dom.node(id).child_nodes() {
match child.node_type() {
NodeType::Text => {
let text = child.node_value().unwrap_or("");
*acc = acc.saturating_add(UnicodeWidthStr::width(text) as u32);
}
NodeType::Element => {
let (display, position) = child
.ext()
.and_then(|e| e.computed.as_ref())
.map(|c| (c.display, c.position))
.unwrap_or((Display::Block, Position::Static));
if display == Display::None
|| matches!(position, Position::Absolute | Position::Fixed)
{
continue;
}
for slot in [StyleSlot::Before, StyleSlot::After] {
let text = generated::static_pseudo_text(dom, child.id(), slot);
*acc = acc
.saturating_add(text.map_or(0, |t| UnicodeWidthStr::width(t) as u32));
}
walk(dom, child.id(), acc);
}
_ => {}
}
}
}
let mut acc: u32 = 0;
walk(dom, id, &mut acc);
acc.min(u16::MAX as u32) as u16
}