use rdom_core::{Dom, NodeId};
use crate::ext::TuiExt;
use crate::layout::{Direction, MarginValue, MinSize, Overflow, Size, clamp_size};
use crate::node::TuiNodeExt;
use crate::render::layout_pass::intrinsic::{content_min_size, intrinsic_size};
use crate::style::ComputedStyle;
pub(super) enum MainNatural {
Fixed(u16),
Flex(u16),
Auto(u16),
}
pub(super) struct ChildMain {
pub(super) id: NodeId,
pub(super) main: MainNatural,
pub(super) min: Option<u16>,
pub(super) max: Option<u16>,
pub(super) main_start_margin: MarginValue,
pub(super) main_end_margin: MarginValue,
}
pub(super) struct MainAxisItems {
pub(super) items: Vec<ChildMain>,
pub(super) consumed_fixed: i32,
pub(super) auto_main_count: u32,
}
pub(super) fn collect_main_axis_items(
dom: &Dom<TuiExt>,
children: &[NodeId],
direction: Direction,
main_budget: u16,
cross_budget: u16,
) -> MainAxisItems {
let mut child_info: Vec<ChildMain> = Vec::with_capacity(children.len());
let mut consumed_fixed: i32 = 0;
let mut auto_main_count: u32 = 0;
for &child in children {
let c = dom
.node(child)
.computed_rc()
.unwrap_or_else(|| std::rc::Rc::new(ComputedStyle::initial()));
let (main_size, min_raw, max) = match direction {
Direction::Row => (c.width.clone(), c.min_width, c.max_width),
Direction::Column => (c.height.clone(), c.min_height, c.max_height),
};
let main_size = match (
direction,
dom.node(child).ext().and_then(|e| e.table_used_width),
) {
(Direction::Row, Some(w)) => Size::Fixed(w),
_ => main_size,
};
let (main_start_m, main_end_m) = match direction {
Direction::Row => (c.margin.left.clone(), c.margin.right.clone()),
Direction::Column => (c.margin.top.clone(), c.margin.bottom.clone()),
};
let main_cb_w = match direction {
Direction::Row => main_budget,
Direction::Column => cross_budget,
};
let margin_consumed = |m: &MarginValue| -> i32 {
if m.is_auto() {
0
} else {
i32::from(m.resolve(main_cb_w))
}
};
consumed_fixed += margin_consumed(&main_start_m) + margin_consumed(&main_end_m);
if matches!(main_start_m, MarginValue::Auto) {
auto_main_count += 1;
}
if matches!(main_end_m, MarginValue::Auto) {
auto_main_count += 1;
}
let natural = match &main_size {
Size::Fixed(n) => MainNatural::Fixed(*n),
Size::Flex(w) => MainNatural::Flex(*w),
Size::Percent(p) => {
let resolved =
Size::percent_of(main_budget as i32, *p).clamp(0, u16::MAX as i32) as u16;
MainNatural::Fixed(resolved)
}
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(main_budget as i32));
let resolved = v.max(0).min(u16::MAX as i32) as u16;
MainNatural::Fixed(resolved)
}
Size::Auto => {
let intrinsic = intrinsic_size(dom, child, direction, cross_budget, main_cb_w);
MainNatural::Auto(intrinsic)
}
};
let min = match min_raw {
None => None,
Some(MinSize::Cells(n)) => Some(n),
Some(MinSize::Auto) => None,
};
if let MainNatural::Fixed(n) | MainNatural::Auto(n) = natural {
consumed_fixed += i32::from(n);
}
let resolve_margin = |m: MarginValue| -> MarginValue {
match m {
MarginValue::Auto => MarginValue::Auto,
MarginValue::Cells(n) => MarginValue::Cells(n),
MarginValue::Calc(_) => MarginValue::Cells(m.resolve(main_cb_w)),
}
};
child_info.push(ChildMain {
id: child,
main: natural,
min,
max,
main_start_margin: resolve_margin(main_start_m),
main_end_margin: resolve_margin(main_end_m),
});
}
MainAxisItems {
items: child_info,
consumed_fixed,
auto_main_count,
}
}
pub(super) struct MainAxisBudget {
pub(super) main: u16,
pub(super) cross: u16,
pub(super) flex_remaining: u16,
pub(super) net: i32,
}
pub(super) fn resolve_flexible_lengths(
dom: &Dom<TuiExt>,
child_info: &[ChildMain],
direction: Direction,
budget: MainAxisBudget,
) -> Vec<u16> {
let mut final_main: Vec<u16> = child_info
.iter()
.map(|ci| match ci.main {
MainNatural::Fixed(n) | MainNatural::Auto(n) => clamp_size(n, ci.min, ci.max),
MainNatural::Flex(_) => 0,
})
.collect();
distribute_grow(child_info, &mut final_main, budget.flex_remaining);
distribute_shrink(dom, child_info, &mut final_main, direction, &budget);
final_main
}
fn distribute_grow(child_info: &[ChildMain], final_main: &mut [u16], flex_remaining: u16) {
let mut frozen: Vec<bool> = child_info
.iter()
.map(|ci| !matches!(ci.main, MainNatural::Flex(_)))
.collect();
let mut budget: u32 = flex_remaining as u32;
loop {
let weight: u32 = child_info
.iter()
.zip(frozen.iter())
.filter(|(_, f)| !**f)
.map(|(ci, _)| match ci.main {
MainNatural::Flex(w) => w as u32,
_ => 0,
})
.sum();
if weight == 0 {
break;
}
let pass_budget = budget;
let mut frozen_this_pass: u32 = 0;
let mut accumulated_weight: u32 = 0;
let mut accumulated: u32 = 0;
let mut clamped_any = false;
for (i, ci) in child_info.iter().enumerate() {
if frozen[i] {
continue;
}
let MainNatural::Flex(w) = ci.main else {
continue;
};
accumulated_weight = accumulated_weight.saturating_add(w as u32);
let target = pass_budget
.saturating_mul(accumulated_weight)
.checked_div(weight)
.unwrap_or(0);
let share = target.saturating_sub(accumulated).min(u16::MAX as u32) as u16;
accumulated = target;
let clamped = clamp_size(share, ci.min, ci.max);
final_main[i] = clamped;
if clamped != share {
frozen[i] = true;
frozen_this_pass = frozen_this_pass.saturating_add(clamped as u32);
clamped_any = true;
}
}
if !clamped_any {
break;
}
budget = budget.saturating_sub(frozen_this_pass);
}
}
fn distribute_shrink(
dom: &Dom<TuiExt>,
child_info: &[ChildMain],
final_main: &mut [u16],
direction: Direction,
budget: &MainAxisBudget,
) {
let net_budget = budget.net;
if net_budget <= 0 {
return;
}
let shrink_of = |ci: &ChildMain| -> u32 {
dom.node(ci.id)
.computed()
.map(|c| c.flex_shrink as u32)
.unwrap_or(1)
};
let basis: Vec<u16> = final_main.to_vec();
let mut frozen: Vec<bool> = child_info.iter().map(|ci| shrink_of(ci) == 0).collect();
let mut floors: Vec<Option<u16>> = vec![None; child_info.len()];
loop {
let total: i32 = final_main.iter().map(|&n| n as i32).sum();
if total <= net_budget {
break;
}
let overflow = (total - net_budget) as u32;
let divisor: u32 = child_info
.iter()
.enumerate()
.filter(|(i, _)| !frozen[*i])
.map(|(i, ci)| (basis[i] as u32) * shrink_of(ci))
.sum();
let Some(divisor) = std::num::NonZeroU32::new(divisor) else {
break; };
let mut accumulated_basis: u32 = 0;
let mut accumulated_shrink: u32 = 0;
let mut clamped_any = false;
for (i, ci) in child_info.iter().enumerate() {
if frozen[i] {
continue;
}
accumulated_basis += (basis[i] as u32) * shrink_of(ci);
let target_total_shrink =
((accumulated_basis as u64 * overflow as u64) / divisor.get() as u64) as u32;
let my_shrink = target_total_shrink.saturating_sub(accumulated_shrink) as u16;
accumulated_shrink = target_total_shrink;
let floor = *floors[i].get_or_insert_with(|| {
ci.min.unwrap_or_else(|| {
resolve_auto_min(dom, ci.id, direction, budget.main, budget.cross)
})
});
let wanted = final_main[i].saturating_sub(my_shrink);
if wanted < floor {
final_main[i] = floor;
frozen[i] = true;
clamped_any = true;
} else {
final_main[i] = wanted;
}
}
if !clamped_any {
break;
}
for i in 0..final_main.len() {
if !frozen[i] {
final_main[i] = basis[i];
}
}
}
}
fn resolve_auto_min(
dom: &Dom<TuiExt>,
id: NodeId,
direction: Direction,
main_budget: u16,
cross_budget: u16,
) -> u16 {
let computed = match dom.node(id).computed() {
Some(c) => c.clone(),
None => return 0,
};
let main_size = match direction {
Direction::Row => &computed.width,
Direction::Column => &computed.height,
};
let overflow_on_axis = match direction {
Direction::Row => computed.overflow_x,
Direction::Column => computed.overflow_y,
};
if overflow_on_axis != Overflow::Visible {
return 0;
}
let specified_cap: Option<u16> = match main_size {
Size::Fixed(n) => Some(*n),
Size::Percent(p) => {
Some(Size::percent_of(main_budget as i32, *p).clamp(0, u16::MAX as i32) as u16)
}
Size::Calc(expr) => {
let v = expr.resolve(&rdom_style::calc::ResolveCtx::new(main_budget as i32));
Some(v.max(0).min(u16::MAX as i32) as u16)
}
Size::Flex(_) => Some(0),
Size::Auto => None,
};
if matches!(specified_cap, Some(0)) {
return 0;
}
let cb_width = match direction {
Direction::Row => main_budget,
Direction::Column => cross_budget,
};
let content = content_min_size(dom, id, direction, cross_budget, cb_width);
match specified_cap {
Some(cap) => content.min(cap),
None => content,
}
}