use crate::geom::{Rect, Size, Vec2};
use crate::scroll::ScrollStore;
use crate::spec::{Align, Dir, FloatAnchor, Min, Sizing};
use crate::tree::{NIL, NodeContent, Tree};
#[inline]
fn is_float(tree: &Tree, i: u32) -> bool {
tree.any_float && tree.specs[i as usize].layout.float.is_some()
}
fn align_factor(a: Align) -> f32 {
match a {
Align::Start | Align::SpaceBetween | Align::Baseline => 0.0,
Align::Center | Align::SpaceAround | Align::SpaceEvenly => 0.5,
Align::End => 1.0,
}
}
fn main_spread(a: Align, free: f32, n: u32) -> (f32, f32) {
let n = n as f32;
match a {
Align::SpaceBetween if n > 1.0 => (0.0, free / (n - 1.0)),
Align::SpaceAround if n > 0.0 => (free / (2.0 * n), free / n),
Align::SpaceEvenly => (free / (n + 1.0), free / (n + 1.0)),
_ => (align_factor(a) * free, 0.0),
}
}
fn mirror(a: Align) -> Align {
match a {
Align::Start => Align::End,
Align::End => Align::Start,
other => other,
}
}
#[inline]
fn baseline_row(tree: &Tree, i: u32) -> bool {
let l = &tree.specs[i as usize].layout;
l.cross_align == Align::Baseline && l.dir == Dir::Row
}
#[inline]
fn aligns_by_baseline(tree: &Tree, c: u32) -> bool {
!matches!(
child_sizing(tree, c, AxisSel::Height),
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_)
)
}
fn first_baseline(tree: &Tree, i: u32) -> Option<f32> {
match tree.content[i as usize] {
NodeContent::Text(_) | NodeContent::Edit(_) => tree
.baseline
.get(i as usize)
.copied()
.filter(|b| b.is_finite()),
NodeContent::Container => {
let f = first_in_flow(tree, i);
if f == NIL {
return None;
}
let spec = &tree.specs[i as usize].layout;
let size = tree.size[i as usize];
let fs = tree.size[f as usize];
match spec.dir {
Dir::Column => {
let fb = first_baseline(tree, f)?;
let content = (size.h - spec.padding.y()).max(0.0);
let (mut used, mut n) = (0.0f32, 0u32);
for c in tree.children(i) {
if !is_float(tree, c) {
used += tree.size[c as usize].h;
n += 1;
}
}
if n > 1 {
used += spec.gap * (n - 1) as f32;
}
let (lead, _) = main_spread(spec.main_align, (content - used).max(0.0), n);
Some(spec.padding.t + lead + fb)
}
Dir::Row => {
let end = if wraps(tree, i) {
line_end(tree, f)
} else {
NIL
};
if spec.cross_align == Align::Baseline {
let (above, _, any) = line_baseline(tree, f, end);
return any.then_some(spec.padding.t + above);
}
let fb = first_baseline(tree, f)?;
let extent = if end == NIL {
(size.h - spec.padding.y()).max(0.0)
} else {
line_extents(tree, f, end, spec.gap).1
};
let off = align_factor(spec.cross_align) * (extent - fs.h).max(0.0);
Some(spec.padding.t + off + fb)
}
}
}
_ => None,
}
}
fn line_baseline(tree: &Tree, c: u32, end: u32) -> (f32, f32, bool) {
let (mut above, mut below, mut any) = (0.0f32, 0.0f32, false);
let mut k = c;
while k != end && k != NIL {
if !is_float(tree, k) && aligns_by_baseline(tree, k) {
let h = tree.size[k as usize].h;
let b = match first_baseline(tree, k) {
Some(b) => {
any = true;
b
}
None => h,
};
above = above.max(b);
below = below.max(h - b);
}
k = tree.next_sibling[k as usize];
}
(above, below, any)
}
fn attach(
anchor_pos: f32,
anchor_len: f32,
self_len: f32,
anchor_pt: Align,
self_pt: Align,
off: f32,
) -> f32 {
anchor_pos + align_factor(anchor_pt) * anchor_len - align_factor(self_pt) * self_len + off
}
fn overflow(pos: f32, len: f32, limit: f32) -> f32 {
(-pos).max(0.0) + (pos + len - limit).max(0.0)
}
#[inline(always)]
fn wraps(tree: &Tree, i: u32) -> bool {
if !tree.any_wrap {
return false;
}
let s = &tree.specs[i as usize].layout;
s.wrap && s.dir == Dir::Row && !s.scroll_x && !is_table_row(tree, i)
}
#[inline]
pub(crate) fn is_table_row(tree: &Tree, i: u32) -> bool {
if !tree.any_table {
return false;
}
let p = tree.parent[i as usize];
p != NIL
&& tree.specs[p as usize].layout.is_table()
&& row_shaped(
&tree.specs[i as usize].layout,
matches!(tree.content[i as usize], NodeContent::Container),
)
}
#[inline]
pub(crate) fn row_shaped(spec: &crate::spec::LayoutSpec, container: bool) -> bool {
spec.dir == Dir::Row && container && spec.float.is_none()
}
#[inline]
fn is_table_cell(tree: &Tree, i: u32) -> bool {
if !tree.any_table {
return false;
}
let p = tree.parent[i as usize];
p != NIL && is_table_row(tree, p) && !is_float(tree, i)
}
#[derive(Clone, Copy, Debug, Default)]
struct Col {
fit: f32,
grow: f32,
pct: f32,
calc: Option<f32>,
fixed: bool,
min: f32,
max: f32,
w: f32,
}
impl Col {
fn clamp(&self, w: f32) -> f32 {
w.clamp(self.min, self.max.max(self.min))
}
}
fn table_columns(tree: &Tree, i: u32, room: Option<f32>) -> Vec<Col> {
let mut cols: Vec<Col> = Vec::new();
for row in tree.children(i) {
if !is_table_row(tree, row) {
continue;
}
let mut j = 0usize;
for cell in tree.children(row) {
if is_float(tree, cell) {
continue;
}
if j == cols.len() {
cols.push(Col {
max: f32::INFINITY,
..Col::default()
});
}
let col = &mut cols[j];
let spec = tree.specs[cell as usize].layout;
col.fit = col.fit.max(tree.size[cell as usize].w);
match child_sizing(tree, cell, AxisSel::Width) {
Sizing::Grow(f) => col.grow = col.grow.max(f.max(0.0)),
Sizing::Percent(p) => col.pct = col.pct.max(p),
Sizing::Calc(c) => {
let px = room.map_or(0.0, |r| c.resolve(r));
col.calc = Some(col.calc.map_or(px, |w| w.max(px)));
}
Sizing::Fixed(_) => col.fixed = true,
Sizing::Fit => {}
}
if !matches!(tree.content[cell as usize], NodeContent::Text(_)) {
col.min = col.min.max(spec.min_w.resolved());
col.max = col.max.min(spec.max_w_px());
}
j += 1;
}
}
cols
}
fn table_row_fit(tree: &Tree, row: u32, cols: &[Col]) -> f32 {
let spec = tree.specs[row as usize].layout;
let mut w = 0.0f32;
let mut n = 0u32;
for cell in tree.children(row) {
if is_float(tree, cell) {
continue;
}
w += cols.get(n as usize).map_or(0.0, |c| c.w);
n += 1;
}
if n > 1 {
w += spec.gap * (n - 1) as f32;
}
w + spec.padding.x()
}
fn table_apply(tree: &mut Tree, i: u32, cols: &[Col], fitting: bool) {
let scrolls = tree.specs[i as usize].layout.scroll_x;
let mut row = tree.first_child[i as usize];
while row != NIL {
if is_table_row(tree, row) {
let mut j = 0usize;
let mut cell = tree.first_child[row as usize];
while cell != NIL {
if !is_float(tree, cell) {
tree.size[cell as usize].w = cols[j].w;
j += 1;
}
cell = tree.next_sibling[cell as usize];
}
let spec = tree.specs[row as usize].layout;
match spec.width {
Sizing::Fit => {
let fit = table_row_fit(tree, row, cols);
tree.size[row as usize].w = spec.clamp_w(fit);
}
Sizing::Fixed(_) => {}
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) if fitting => {
let fit = table_row_fit(tree, row, cols);
tree.size[row as usize].w = spec.clamp_w(fit);
}
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => {}
}
if scrolls && !fitting {
let fit = table_row_fit(tree, row, cols);
let w = &mut tree.size[row as usize].w;
*w = w.max(fit);
}
}
row = tree.next_sibling[row as usize];
}
}
fn table_fit(tree: &mut Tree, i: u32) {
let mut cols = table_columns(tree, i, None);
for col in &mut cols {
col.w = col.clamp(col.fit);
}
table_apply(tree, i, &cols, true);
}
fn table_resolve(tree: &mut Tree, i: u32) {
let n = tree
.children(i)
.filter(|&row| is_table_row(tree, row))
.map(|row| tree.children(row).filter(|&c| !is_float(tree, c)).count())
.max()
.unwrap_or(0);
if n == 0 {
return;
}
let mut avail = 0.0f32;
for row in tree.children(i) {
if !is_table_row(tree, row) {
continue;
}
let spec = tree.specs[row as usize].layout;
let chrome = spec.padding.x() + spec.gap * (n as f32 - 1.0);
avail = avail.max(tree.size[row as usize].w - chrome);
}
let avail = avail.max(0.0);
if tree.any_calc_bound {
for row in tree.children(i).collect::<Vec<_>>() {
if !is_table_row(tree, row) {
continue;
}
for cell in tree.children(row).collect::<Vec<_>>() {
if !is_float(tree, cell) {
resolve_bounds(tree, cell, AxisSel::Width, avail);
}
}
}
}
let mut cols = table_columns(tree, i, Some(avail));
debug_assert_eq!(cols.len(), n);
let mut used = 0.0f32;
let mut grow_total = 0.0f32;
for col in &mut cols {
if col.grow > 0.0 {
grow_total += col.grow;
col.w = col.clamp(0.0);
} else if col.pct > 0.0 || col.calc.is_some() {
let calc = col.calc.unwrap_or(0.0);
col.w = col.clamp((avail * col.pct).max(calc));
used += col.w;
} else {
col.w = col.clamp(col.fit);
used += col.w;
}
}
if grow_total > 0.0 {
let mut frozen = vec![false; cols.len()];
loop {
let remain = (avail - used).max(0.0);
let mut froze = false;
for (j, col) in cols.iter_mut().enumerate() {
if col.grow <= 0.0 || frozen[j] {
continue;
}
let share = remain * col.grow / grow_total;
col.w = col.clamp(share);
if (col.w - share).abs() > 0.01 {
frozen[j] = true;
used += col.w;
grow_total -= col.grow;
froze = true;
}
}
if !froze || grow_total <= 0.0 {
break;
}
}
}
let total: f32 = cols.iter().map(|c| c.w).sum();
let mut deficit = total - avail;
if deficit > 0.5 && !tree.specs[i as usize].layout.scroll_x {
let shrinkable = |c: &Col| !c.fixed && c.grow <= 0.0 && c.pct <= 0.0 && c.calc.is_none();
let mut guard = 0;
while deficit > 0.5 && guard < 128 {
guard += 1;
let mut largest = f32::NEG_INFINITY;
let mut second = 0.0f32;
let mut count = 0u32;
for c in cols.iter().filter(|c| shrinkable(c) && c.w > c.min + 0.01) {
if c.w > largest + 0.01 {
second = if largest.is_finite() {
largest.max(second)
} else {
second
};
largest = c.w;
count = 1;
} else if c.w > largest - 0.01 {
count += 1;
} else if c.w > second {
second = c.w;
}
}
if count == 0 {
break;
}
let target = (largest - deficit / count as f32).max(second).max(0.0);
let mut shrunk_any = false;
for c in cols.iter_mut().filter(|c| shrinkable(c)) {
if c.w > largest - 0.01 {
let new = target.max(c.min);
if new < c.w {
deficit -= c.w - new;
c.w = new;
shrunk_any = true;
}
}
}
if !shrunk_any {
break;
}
}
}
table_apply(tree, i, &cols, false);
}
fn first_in_flow(tree: &Tree, i: u32) -> u32 {
let mut c = tree.first_child[i as usize];
while c != NIL && is_float(tree, c) {
c = tree.next_sibling[c as usize];
}
c
}
fn line_end(tree: &Tree, c: u32) -> u32 {
let l = tree.line[c as usize];
let mut n = tree.next_sibling[c as usize];
while n != NIL && (is_float(tree, n) || tree.line[n as usize] == l) {
n = tree.next_sibling[n as usize];
}
n
}
fn line_extents(tree: &Tree, c: u32, end: u32, gap: f32) -> (f32, f32) {
let baseline = tree.any_baseline && c != NIL && baseline_row(tree, tree.parent[c as usize]);
let mut main = 0.0f32;
let mut cross = 0.0f32;
let mut n = 0u32;
let mut k = c;
while k != end && k != NIL {
if !is_float(tree, k) {
let size = tree.size[k as usize];
main += size.w;
if !matches!(
child_sizing(tree, k, AxisSel::Height),
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_)
) {
cross = cross.max(size.h);
}
n += 1;
}
k = tree.next_sibling[k as usize];
}
if n > 1 {
main += gap * (n - 1) as f32;
}
if baseline {
let (above, below, _) = line_baseline(tree, c, end);
cross = cross.max(above + below);
}
(main, cross)
}
fn wrap_measure(tree: &Tree, i: u32) -> (u32, f32, f32) {
let spec = tree.specs[i as usize].layout;
let mut lines = 0u32;
let mut stacked = 0.0f32;
let mut widest = 0.0f32;
let mut c = first_in_flow(tree, i);
while c != NIL {
let end = line_end(tree, c);
let (main, cross) = line_extents(tree, c, end, spec.gap);
stacked += cross;
widest = widest.max(main);
lines += 1;
c = end;
}
if lines > 1 {
stacked += spec.cross_gap * (lines - 1) as f32;
}
(lines, stacked, widest)
}
fn line_stretch(lines: u32, stacked: f32, cross_content: f32) -> f32 {
if lines == 0 {
0.0
} else {
(cross_content - stacked).max(0.0) / lines as f32
}
}
fn break_lines(tree: &mut Tree, i: u32, content: f32, gap: f32) {
let mut line = 0u32;
let mut used = 0.0f32;
let mut n = 0u32;
let mut c = tree.first_child[i as usize];
while c != NIL {
if !is_float(tree, c) {
let base = tree.size[c as usize].w;
let needed = if n > 0 { gap + base } else { base };
if n > 0 && used + needed > content + 0.01 {
line += 1;
used = base;
n = 1;
} else {
used += needed;
n += 1;
}
tree.line[c as usize] = line;
}
c = tree.next_sibling[c as usize];
}
}
pub trait TextMeasure {
fn intrinsic(&mut self, id: crate::tree::TextId) -> Size;
fn wrapped(&mut self, id: crate::tree::TextId, max_w: f32) -> Size;
fn min_content(&mut self, _id: crate::tree::TextId) -> f32 {
0.0
}
fn edit_intrinsic(&mut self, _key: crate::key::Key) -> Size {
Size::ZERO
}
fn edit_wrapped(&mut self, _key: crate::key::Key, _max_w: f32) -> Size {
Size::ZERO
}
fn baseline(&mut self, _id: crate::tree::TextId) -> f32 {
f32::NAN
}
fn edit_baseline(&mut self, _key: crate::key::Key) -> f32 {
f32::NAN
}
fn image_size(&mut self, _id: crate::resources::ImageId) -> Size {
Size::ZERO
}
fn cells_size(&mut self, _id: crate::cells::CellsId) -> Size {
Size::ZERO
}
}
pub fn compute(
tree: &mut Tree,
text: &mut dyn TextMeasure,
scroll: &mut ScrollStore,
viewport: Size,
scale: f32,
) {
if tree.is_empty() {
return;
}
let declared = if tree.any_node_float {
declared_clamps(tree)
} else {
Vec::new()
};
fit_widths(tree, text, 0..tree.len());
grow_widths(tree, text, viewport);
fit_heights(tree, text, 0..tree.len());
grow_heights(tree, text, viewport);
positions(tree, scroll, viewport, scale, 0..tree.len());
if tree.any_node_float {
anchored(tree, text, scroll, viewport, scale, &declared);
}
}
type Declared = (usize, Min, f32, Min, f32);
fn declared_clamps(tree: &Tree) -> Vec<Declared> {
let mut out = Vec::new();
for (c, end, _) in node_floats(tree) {
for i in c..end {
let l = &tree.specs[i].layout;
if l.min_w.deferred() || l.min_h.deferred() || l.max_w < 0.0 || l.max_h < 0.0 {
out.push((i, l.min_w, l.max_w, l.min_h, l.max_h));
}
}
}
out
}
fn anchored(
tree: &mut Tree,
text: &mut dyn TextMeasure,
scroll: &mut ScrollStore,
viewport: Size,
scale: f32,
declared: &[Declared],
) {
for (c, end, key) in node_floats(tree) {
let Some(a) = tree.index_of(key) else {
tree.size[c] = Size::default();
continue;
};
let anchor = Rect::from_pos_size(tree.pos[a], tree.size[a]);
for &(i, min_w, max_w, min_h, max_h) in
declared.iter().filter(|(i, ..)| (c..end).contains(i))
{
let l = &mut tree.specs[i].layout;
(l.min_w, l.max_w, l.min_h, l.max_h) = (min_w, max_w, min_h, max_h);
}
fit_widths(tree, text, c..end);
if tree.any_calc_bound {
resolve_bounds(tree, c as u32, AxisSel::Width, anchor.w);
}
let spec = tree.specs[c].layout;
tree.size[c].w = spec.clamp_w(match spec.width {
Sizing::Grow(_) => anchor.w,
s => of_room(s, anchor.w).unwrap_or(tree.size[c].w),
});
for i in c..end {
distribute_axis(tree, text, i as u32, AxisSel::Width, viewport);
}
fit_heights(tree, text, c..end);
if tree.any_calc_bound {
resolve_bounds(tree, c as u32, AxisSel::Height, anchor.h);
}
let spec = tree.specs[c].layout;
tree.size[c].h = spec.clamp_h(match spec.height {
Sizing::Grow(_) => anchor.h,
s => of_room(s, anchor.h).unwrap_or(tree.size[c].h),
});
for i in c..end {
distribute_axis(tree, text, i as u32, AxisSel::Height, viewport);
}
place_anchored(tree, c, anchor, viewport);
positions(tree, scroll, viewport, scale, c..end);
}
}
fn node_floats(tree: &Tree) -> Vec<(usize, usize, crate::key::Key)> {
let mut out = Vec::new();
let mut c = 0usize;
while c < tree.len() {
if let Some(crate::spec::FloatConfig {
anchor: FloatAnchor::Node(key),
..
}) = tree.specs[c].layout.float
{
let mut end = c + 1;
while end < tree.len() && (tree.parent[end] as usize) >= c {
end += 1;
}
out.push((c, end, key));
c = end;
} else {
c += 1;
}
}
out
}
fn place_anchored(tree: &mut Tree, c: usize, anchor: Rect, viewport: Size) {
let Some(cfg) = tree.specs[c].layout.float else {
return;
};
let vp = float_viewport(tree, c as u32, viewport);
tree.pos[c] = attach_fitted(&cfg, anchor, tree.size[c], vp, true);
}
pub(crate) fn reposition(tree: &mut Tree, scroll: &mut ScrollStore, viewport: Size, scale: f32) {
positions(tree, scroll, viewport, scale, 0..tree.len());
if tree.any_node_float {
for (c, end, key) in node_floats(tree) {
if let Some(a) = tree.index_of(key) {
let anchor = Rect::from_pos_size(tree.pos[a], tree.size[a]);
place_anchored(tree, c, anchor, viewport);
positions(tree, scroll, viewport, scale, c..end);
}
}
}
}
#[inline(always)]
fn fit_width(tree: &Tree, i: usize, text: &mut dyn TextMeasure) -> f32 {
let spec = &tree.specs[i].layout;
match tree.content[i] {
NodeContent::Edit(key) => text.edit_intrinsic(key).w + spec.padding.x(),
NodeContent::Image(id, _) => text.image_size(id).w,
NodeContent::Cells(id) => text.cells_size(id).w + spec.padding.x(),
_ => {
let mut w = 0.0f32;
let mut n = 0u32;
for c in tree.children(i as u32) {
if is_float(tree, c) {
continue;
}
let cw = tree.size[c as usize].w;
if spec.dir == Dir::Row {
w += cw;
} else {
w = w.max(cw);
}
n += 1;
}
if spec.dir == Dir::Row && n > 1 {
w += spec.gap * (n - 1) as f32;
}
w + spec.padding.x()
}
}
}
fn fit_widths(tree: &mut Tree, text: &mut dyn TextMeasure, range: std::ops::Range<usize>) {
for i in range.rev() {
if let NodeContent::Text(tid) = tree.content[i] {
tree.size[i].w = text.intrinsic(tid).w;
continue;
}
if tree.any_table && tree.specs[i].layout.is_table() {
table_fit(tree, i as u32);
}
let width = tree.specs[i].layout.width;
let min_fit = tree.specs[i].layout.min_w.is_fit();
let fit = if min_fit || width == Sizing::Fit {
fit_width(tree, i, text)
} else {
0.0
};
if min_fit {
tree.specs[i].layout.min_w = Min::px(fit);
}
let spec = &tree.specs[i].layout;
tree.size[i].w = spec.clamp_w(match width {
Sizing::Fixed(px) => px,
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => 0.0,
Sizing::Fit => spec.aspect_width().unwrap_or(fit),
});
}
}
fn min_content(tree: &Tree, text: &mut dyn TextMeasure, i: usize, axis: AxisSel) -> f32 {
let spec = &tree.specs[i].layout;
let (pad, scrolls) = match axis {
AxisSel::Width => (spec.padding.x(), spec.scroll_x),
AxisSel::Height => (spec.padding.y(), spec.scroll_y),
};
let own = match (tree.content[i], axis) {
(NodeContent::Text(tid), AxisSel::Width) => return text.min_content(tid),
(
NodeContent::Text(_) | NodeContent::Image(..) | NodeContent::Cells(_),
AxisSel::Height,
) => {
return tree.size[i].h;
}
(NodeContent::Edit(_), AxisSel::Width) => 0.0,
(NodeContent::Edit(key), AxisSel::Height) => {
text.edit_wrapped(key, (tree.size[i].w - spec.padding.x()).max(0.0))
.h
}
(NodeContent::Image(id, _), AxisSel::Width) => text.image_size(id).w,
(NodeContent::Cells(id), AxisSel::Width) => text.cells_size(id).w,
_ if scrolls || spec.clip => 0.0,
_ => children_min_content(tree, text, i, axis),
};
own + pad
}
fn children_min_content(tree: &Tree, text: &mut dyn TextMeasure, i: usize, axis: AxisSel) -> f32 {
let spec = &tree.specs[i].layout;
let main = (spec.dir == Dir::Row) == (axis == AxisSel::Width);
let adds = main && !(spec.wrap && axis == AxisSel::Width);
let lines = axis == AxisSel::Height && wraps(tree, i as u32);
let (mut total, mut n) = (0.0f32, 0u32);
let (mut line, mut tallest, mut stacked) = (None, 0.0f32, 0u32);
let mut c = tree.first_child[i];
while c != NIL {
if !is_float(tree, c) {
let l = &tree.specs[c as usize].layout;
let (sizing, min, max) = match axis {
AxisSel::Width => (l.width, l.min_w, l.max_w_px()),
AxisSel::Height => (l.height, l.min_h, l.max_h_px()),
};
let own = match sizing {
Sizing::Fixed(px) => px,
_ => min_content(tree, text, c as usize, axis),
};
let v = own.min(max).max(min.resolved());
if lines {
let l = tree.line[c as usize];
if line != Some(l) {
total += tallest;
(line, tallest) = (Some(l), 0.0);
stacked += 1;
}
tallest = tallest.max(v);
} else if adds {
total += v;
} else {
total = total.max(v);
}
n += 1;
}
c = tree.next_sibling[c as usize];
}
if lines {
total += tallest + spec.cross_gap * stacked.saturating_sub(1) as f32;
} else if adds && n > 1 {
total += spec.gap * (n - 1) as f32;
}
total
}
#[inline(always)]
fn fit_height(tree: &Tree, i: usize, text: &mut dyn TextMeasure, edit: Size) -> f32 {
let spec = &tree.specs[i].layout;
match tree.content[i] {
NodeContent::Edit(_) => edit.h + spec.padding.y(),
NodeContent::Cells(id) => text.cells_size(id).h + spec.padding.y(),
NodeContent::Image(id, _) => {
let intrinsic = text.image_size(id);
if intrinsic.w <= 0.0 {
return 0.0;
}
let w = if is_table_cell(tree, i as u32) {
match spec.width {
Sizing::Fixed(px) => spec.clamp_w(px),
Sizing::Fit => spec.clamp_w(intrinsic.w),
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => tree.size[i].w,
}
} else {
tree.size[i].w
};
intrinsic.h * w / intrinsic.w
}
_ if wraps(tree, i as u32) => wrap_measure(tree, i as u32).1 + spec.padding.y(),
_ if tree.any_baseline && baseline_row(tree, i as u32) => {
let f = first_in_flow(tree, i as u32);
line_extents(tree, f, NIL, spec.gap).1 + spec.padding.y()
}
_ => {
let mut h = 0.0f32;
let mut n = 0u32;
for c in tree.children(i as u32) {
if is_float(tree, c) {
continue;
}
let ch = tree.size[c as usize].h;
if spec.dir == Dir::Column {
h += ch;
} else {
h = h.max(ch);
}
n += 1;
}
if spec.dir == Dir::Column && n > 1 {
h += spec.gap * (n - 1) as f32;
}
h + spec.padding.y()
}
}
}
fn fit_heights(tree: &mut Tree, text: &mut dyn TextMeasure, range: std::ops::Range<usize>) {
if tree.any_baseline {
let n = tree.len();
tree.baseline.resize(n, f32::NAN);
}
for i in range.rev() {
if let NodeContent::Text(tid) = tree.content[i] {
let wrapped = text.wrapped(tid, tree.size[i].w.max(0.0));
if is_table_cell(tree, i as u32) {
tree.size[i].h = wrapped.h;
tree.size[i].w = tree.size[i].w.max(wrapped.w);
} else {
tree.size[i] = wrapped;
}
if tree.any_baseline {
tree.baseline[i] = text.baseline(tid);
}
continue;
}
let edit = if let NodeContent::Edit(key) = tree.content[i] {
let inner = (tree.size[i].w - tree.specs[i].layout.padding.x()).max(0.0);
let wrapped = text.edit_wrapped(key, inner);
if tree.any_baseline {
tree.baseline[i] = tree.specs[i].layout.padding.t + text.edit_baseline(key);
}
wrapped
} else {
Size::default()
};
let height = tree.specs[i].layout.height;
let min_fit = tree.specs[i].layout.min_h.is_fit();
let fit = if min_fit || height == Sizing::Fit {
fit_height(tree, i, text, edit)
} else {
0.0
};
if min_fit {
tree.specs[i].layout.min_h = Min::px(fit);
}
let spec = &tree.specs[i].layout;
tree.size[i].h = spec.clamp_h(match height {
Sizing::Fixed(px) => px,
Sizing::Grow(_) | Sizing::Percent(_) | Sizing::Calc(_) => 0.0,
Sizing::Fit if spec.aspect_height() => tree.size[i].w / spec.aspect,
Sizing::Fit => fit,
});
}
}
fn grow_widths(tree: &mut Tree, text: &mut dyn TextMeasure, viewport: Size) {
for i in 0..tree.len() {
if tree.parent[i] == NIL {
if tree.any_calc_bound {
resolve_bounds(tree, i as u32, AxisSel::Width, viewport.w);
}
let spec = tree.specs[i].layout;
tree.size[i].w = spec.clamp_w(resolve_root(spec.width, tree.size[i].w, viewport.w));
}
distribute_axis(tree, text, i as u32, AxisSel::Width, viewport);
}
}
fn grow_heights(tree: &mut Tree, text: &mut dyn TextMeasure, viewport: Size) {
for i in 0..tree.len() {
if tree.parent[i] == NIL {
if tree.any_calc_bound {
resolve_bounds(tree, i as u32, AxisSel::Height, viewport.h);
}
let spec = tree.specs[i].layout;
tree.size[i].h = spec.clamp_h(resolve_root(spec.height, tree.size[i].h, viewport.h));
}
distribute_axis(tree, text, i as u32, AxisSel::Height, viewport);
}
}
fn resolve_root(sizing: Sizing, fitted: f32, viewport: f32) -> f32 {
match sizing {
Sizing::Grow(_) => viewport,
Sizing::Percent(p) => viewport * p,
Sizing::Calc(c) => c.resolve(viewport),
Sizing::Fixed(px) => px,
Sizing::Fit => fitted,
}
}
#[derive(Clone, Copy, PartialEq)]
enum AxisSel {
Width,
Height,
}
fn distribute_axis(
tree: &mut Tree,
text: &mut dyn TextMeasure,
i: u32,
axis: AxisSel,
viewport: Size,
) {
let spec = tree.specs[i as usize].layout;
let (own, pad) = match axis {
AxisSel::Width => (tree.size[i as usize].w, spec.padding.x()),
AxisSel::Height => (tree.size[i as usize].h, spec.padding.y()),
};
let content = (own - pad).max(0.0);
let is_main = (spec.dir == Dir::Row) == (axis == AxisSel::Width);
if tree.any_calc_bound {
let mut c = tree.first_child[i as usize];
while c != NIL {
if !is_float(tree, c) && resolve_bounds(tree, c, axis, content) {
let now = get_axis(tree, c, axis);
set_axis_clamped(tree, c, axis, now);
}
c = tree.next_sibling[c as usize];
}
}
if is_main && axis == AxisSel::Width && is_table_row(tree, i) {
} else if is_main {
let mut share = false;
let mut c = tree.first_child[i as usize];
while c != NIL {
if !is_float(tree, c)
&& let Some(px) = of_room(child_sizing(tree, c, axis), content)
{
set_axis_clamped(tree, c, axis, px);
share = true;
}
c = tree.next_sibling[c as usize];
}
let scrolls = match axis {
AxisSel::Width => spec.scroll_x,
AxisSel::Height => spec.scroll_y,
};
if wraps(tree, i) {
break_lines(tree, i, content, spec.gap);
let mut c = first_in_flow(tree, i);
while c != NIL {
let end = line_end(tree, c);
let line = tree.line[c as usize];
let total = distribute_run(tree, c, end, axis, content, spec.gap);
let deficit = total - content;
if deficit > 0.5 {
shrink_axis(tree, text, i, axis, deficit, Some(line), share);
}
c = end;
}
} else {
let total = distribute_run(
tree,
tree.first_child[i as usize],
NIL,
axis,
content,
spec.gap,
);
let deficit = total - content;
if deficit > 0.5 && !scrolls {
shrink_axis(tree, text, i, axis, deficit, None, share);
}
}
} else if wraps(tree, i) {
let (lines, stacked, _) = wrap_measure(tree, i);
let stretch = line_stretch(lines, stacked, content);
let mut c = first_in_flow(tree, i);
while c != NIL {
let end = line_end(tree, c);
let extent = line_extents(tree, c, end, spec.gap).1 + stretch;
let mut k = c;
while k != end && k != NIL {
if !is_float(tree, k) {
match child_sizing(tree, k, axis) {
Sizing::Grow(_) => set_axis_clamped(tree, k, axis, extent),
s => {
if let Some(px) = of_room(s, extent) {
set_axis_clamped(tree, k, axis, px);
}
}
}
}
k = tree.next_sibling[k as usize];
}
c = end;
}
} else {
let fits = axis == AxisSel::Width && !spec.scroll_x;
let mut c = tree.first_child[i as usize];
while c != NIL {
if !is_float(tree, c) {
match child_sizing(tree, c, axis) {
Sizing::Grow(_) => set_axis_clamped(tree, c, axis, content),
Sizing::Fit
if fits
&& tree.size[c as usize].w > content
&& !matches!(
tree.content[c as usize],
NodeContent::Text(_)
| NodeContent::Cells(_)
| NodeContent::Image(..)
)
&& tree.specs[c as usize].layout.aspect_width().is_none() =>
{
set_axis_clamped(tree, c, axis, content)
}
s => {
if let Some(px) = of_room(s, content) {
set_axis_clamped(tree, c, axis, px);
}
}
}
}
c = tree.next_sibling[c as usize];
}
if axis == AxisSel::Width && tree.any_table && spec.is_table() {
table_resolve(tree, i);
}
}
let mut c = if tree.any_float {
tree.first_child[i as usize]
} else {
NIL
};
while c != NIL {
if let Some(cfg) = tree.specs[c as usize].layout.float {
let vp = float_viewport(tree, c, viewport);
let anchor_dim = match (cfg.anchor, axis) {
(FloatAnchor::Parent, AxisSel::Width) => tree.size[i as usize].w,
(FloatAnchor::Parent, AxisSel::Height) => tree.size[i as usize].h,
(FloatAnchor::Viewport, AxisSel::Width) => vp.w,
(FloatAnchor::Viewport, AxisSel::Height) => vp.h,
(FloatAnchor::Node(_), _) => 0.0,
};
if tree.any_calc_bound && resolve_bounds(tree, c, axis, anchor_dim) {
let now = get_axis(tree, c, axis);
set_axis_clamped(tree, c, axis, now);
}
match child_sizing(tree, c, axis) {
Sizing::Grow(_) => set_axis_clamped(tree, c, axis, anchor_dim),
s => {
if let Some(px) = of_room(s, anchor_dim) {
set_axis_clamped(tree, c, axis, px);
}
}
}
}
c = tree.next_sibling[c as usize];
}
if axis == AxisSel::Width && tree.any_text {
let mut c = tree.first_child[i as usize];
while c != NIL {
if matches!(tree.content[c as usize], NodeContent::Text(_))
&& tree.size[c as usize].w > content
{
tree.size[c as usize].w = content;
}
c = tree.next_sibling[c as usize];
}
}
}
fn distribute_run(
tree: &mut Tree,
start: u32,
end: u32,
axis: AxisSel,
content: f32,
gap: f32,
) -> f32 {
let mut used = 0.0f32;
let mut grow_total = 0.0f32;
let mut n = 0u32;
let mut c = start;
while c != end && c != NIL {
if !is_float(tree, c) {
match child_sizing(tree, c, axis) {
Sizing::Grow(f) => grow_total += f.max(0.0),
_ => used += get_axis(tree, c, axis),
}
n += 1;
}
c = tree.next_sibling[c as usize];
}
if n > 1 {
used += gap * (n - 1) as f32;
}
let mut total = used;
if grow_total > 0.0 {
const FROZEN: u8 = 1;
const MIN_VIOLATOR: u8 = 2;
const MAX_VIOLATOR: u8 = 4;
let mut scratch = std::mem::take(&mut tree.grow_scratch);
scratch.clear();
scratch.resize(n as usize, 0);
loop {
let remain = (content - used).max(0.0);
let mut violation = 0.0f32;
let mut unfrozen = 0.0f32;
let mut k = 0usize;
let mut c = start;
while c != end && c != NIL {
if !is_float(tree, c) {
if scratch[k] & FROZEN == 0
&& let Sizing::Grow(f) = child_sizing(tree, c, axis)
{
let share = remain * f.max(0.0) / grow_total;
set_axis_clamped(tree, c, axis, share);
let got = get_axis(tree, c, axis);
unfrozen += got;
let off = got - share;
scratch[k] = if off > 0.01 {
violation += off;
MIN_VIOLATOR
} else if off < -0.01 {
violation += off;
MAX_VIOLATOR
} else {
0
};
}
k += 1;
}
c = tree.next_sibling[c as usize];
}
if violation.abs() <= 0.01 {
total = used + unfrozen;
break;
}
let freeze = if violation > 0.0 {
MIN_VIOLATOR
} else {
MAX_VIOLATOR
};
let mut k = 0usize;
let mut c = start;
while c != end && c != NIL {
if !is_float(tree, c) {
if scratch[k] & freeze != 0
&& let Sizing::Grow(f) = child_sizing(tree, c, axis)
{
scratch[k] = FROZEN;
used += get_axis(tree, c, axis);
grow_total -= f.max(0.0);
}
k += 1;
}
c = tree.next_sibling[c as usize];
}
if grow_total <= 0.0 {
total = used;
break;
}
}
tree.grow_scratch = scratch;
}
total
}
fn shrink_axis(
tree: &mut Tree,
text: &mut dyn TextMeasure,
i: u32,
axis: AxisSel,
mut deficit: f32,
only_line: Option<u32>,
share: bool,
) {
let shrinkable = |tree: &Tree, c: u32| -> Option<f32> {
if is_float(tree, c)
|| !matches!(
child_sizing(tree, c, axis),
Sizing::Fit | Sizing::Percent(_) | Sizing::Calc(_)
)
{
return None;
}
if only_line.is_some_and(|l| tree.line[c as usize] != l) {
return None;
}
if axis == AxisSel::Height
&& matches!(
tree.content[c as usize],
NodeContent::Text(_) | NodeContent::Edit(_) | NodeContent::Image(..)
)
{
return None;
}
let spec = tree.specs[c as usize].layout;
let derived = match axis {
AxisSel::Width => spec.aspect_width().is_some(),
AxisSel::Height => spec.aspect_height(),
};
if derived {
return None;
}
Some(match axis {
AxisSel::Width => spec.min_w.resolved(),
AxisSel::Height => spec.min_h.resolved(),
})
};
if share {
let mut c = tree.first_child[i as usize];
let mut items = std::mem::take(&mut tree.shrink_scratch);
items.clear();
let mut share = false;
while c != NIL {
if let Some(declared) = shrinkable(tree, c) {
share |= matches!(
child_sizing(tree, c, axis),
Sizing::Percent(_) | Sizing::Calc(_)
);
let l = &tree.specs[c as usize].layout;
let (min, scrolls) = match axis {
AxisSel::Width => (l.min_w, l.scroll_x),
AxisSel::Height => (l.min_h, l.scroll_y),
};
let base = get_axis(tree, c, axis);
let floor = match () {
_ if !min.is_auto() => declared,
_ if scrolls => 0.0,
_ => min_content(tree, text, c as usize, axis),
};
items.push(Give {
c,
base,
floor: floor.min(base),
size: base,
held: false,
});
}
c = tree.next_sibling[c as usize];
}
if share {
shrink_as_css(tree, axis, deficit, &mut items);
}
tree.shrink_scratch = items;
if share {
return;
}
}
let mut items = std::mem::take(&mut tree.shrink_scratch);
items.clear();
let mut c = tree.first_child[i as usize];
while c != NIL {
if let Some(declared) = shrinkable(tree, c) {
let base = get_axis(tree, c, axis);
let l = &tree.specs[c as usize].layout;
let floor = match axis {
AxisSel::Height if l.min_h.is_auto() => {
if l.scroll_y || l.clip {
0.0
} else {
min_content(tree, text, c as usize, axis)
}
}
_ => declared,
};
items.push(Give {
c,
base,
floor: floor.min(base),
size: base,
held: false,
});
}
c = tree.next_sibling[c as usize];
}
let mut guard = 0;
while deficit > 0.5 && guard < 128 {
guard += 1;
let mut largest = f32::NEG_INFINITY;
let mut second = 0.0f32;
let mut count = 0u32;
for g in items.iter().filter(|g| g.size > g.floor + 0.01) {
let s = g.size;
if s > largest + 0.01 {
second = if largest.is_finite() {
largest.max(second)
} else {
second
};
largest = s;
count = 1;
} else if s > largest - 0.01 {
count += 1;
} else if s > second {
second = s;
}
}
if count == 0 {
break;
}
let target = (largest - deficit / count as f32).max(second).max(0.0);
let mut shrunk_any = false;
for g in items.iter_mut().filter(|g| g.size > largest - 0.01) {
let new = target.max(g.floor);
if new < g.size {
deficit -= g.size - new;
g.size = new;
shrunk_any = true;
}
}
if !shrunk_any {
break;
}
}
for g in items.iter() {
if g.size < g.base {
set_axis(tree, g.c, axis, g.size);
}
}
tree.shrink_scratch = items;
}
#[inline(never)]
fn shrink_as_css(tree: &mut Tree, axis: AxisSel, deficit: f32, items: &mut [Give]) {
for _ in 0..=items.len() {
let (mut paid, mut weight) = (0.0f32, 0.0f32);
for g in items.iter() {
if g.held {
paid += g.base - g.size;
} else {
weight += g.base;
}
}
let left = deficit - paid;
if left <= 0.0 || weight <= 0.0 {
break;
}
let mut under = false;
for g in items.iter_mut().filter(|g| !g.held) {
let want = g.base - left * g.base / weight;
if want < g.floor {
(g.size, g.held, under) = (g.floor, true, true);
} else {
g.size = want;
}
}
if !under {
break;
}
}
for g in items.iter() {
if g.size < g.base {
set_axis(tree, g.c, axis, g.size);
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Give {
c: u32,
base: f32,
floor: f32,
size: f32,
held: bool,
}
#[inline]
fn of_room(sizing: Sizing, room: f32) -> Option<f32> {
match sizing {
Sizing::Percent(p) => Some(room * p),
Sizing::Calc(c) => Some(c.resolve(room)),
_ => None,
}
}
fn resolve_bounds(tree: &mut Tree, c: u32, axis: AxisSel, room: f32) -> bool {
let l = &mut tree.specs[c as usize].layout;
let (min, max) = match axis {
AxisSel::Width => (&mut l.min_w, &mut l.max_w),
AxisSel::Height => (&mut l.min_h, &mut l.max_h),
};
let mut any = false;
if let Some(k) = min.as_calc() {
*min = Min::px(k.resolve(room));
any = true;
}
if let Some(k) = crate::spec::max_calc(*max) {
*max = k.resolve(room);
any = true;
}
any
}
fn child_sizing(tree: &Tree, c: u32, axis: AxisSel) -> Sizing {
if matches!(tree.content[c as usize], NodeContent::Text(_)) {
return Sizing::Fit;
}
match axis {
AxisSel::Width => tree.specs[c as usize].layout.width,
AxisSel::Height => tree.specs[c as usize].layout.height,
}
}
fn get_axis(tree: &Tree, c: u32, axis: AxisSel) -> f32 {
match axis {
AxisSel::Width => tree.size[c as usize].w,
AxisSel::Height => tree.size[c as usize].h,
}
}
fn set_axis(tree: &mut Tree, c: u32, axis: AxisSel, v: f32) {
match axis {
AxisSel::Width => tree.size[c as usize].w = v,
AxisSel::Height => tree.size[c as usize].h = v,
}
}
#[inline]
fn set_axis_clamped(tree: &mut Tree, c: u32, axis: AxisSel, v: f32) {
let spec = &tree.specs[c as usize].layout;
let v = match axis {
AxisSel::Width => spec.clamp_w(v),
AxisSel::Height => spec.clamp_h(v),
};
set_axis(tree, c, axis, v);
}
fn float_viewport(tree: &Tree, c: u32, viewport: Size) -> Rect {
use crate::tree::OriginId;
let window = Rect::new(0.0, 0.0, viewport.w, viewport.h);
let origin = tree.origins[c as usize];
if tree.host_area.w <= 0.0 || origin == OriginId::DEVTOOLS || origin == OriginId::MENU {
window
} else {
tree.host_area
}
}
fn place_float(
tree: &mut Tree,
cfg: crate::spec::FloatConfig,
c: u32,
parent: Rect,
viewport: Size,
) {
let vp = float_viewport(tree, c, viewport);
let anchor = match cfg.anchor {
FloatAnchor::Parent => parent,
FloatAnchor::Viewport => vp,
FloatAnchor::Node(_) => return,
};
let cs = tree.size[c as usize];
let mirrors = cfg.anchor == FloatAnchor::Parent;
tree.pos[c as usize] = attach_fitted(&cfg, anchor, cs, vp, mirrors);
}
fn attach_fitted(
cfg: &crate::spec::FloatConfig,
anchor: Rect,
cs: Size,
vp: Rect,
mirrors: bool,
) -> Vec2 {
let mut x = attach(
anchor.x,
anchor.w,
cs.w,
cfg.anchor_point.0,
cfg.self_point.0,
cfg.offset.x,
);
let mut y = attach(
anchor.y,
anchor.h,
cs.h,
cfg.anchor_point.1,
cfg.self_point.1,
cfg.offset.y,
);
if cfg.fit && mirrors {
let fx = attach(
anchor.x,
anchor.w,
cs.w,
mirror(cfg.anchor_point.0),
mirror(cfg.self_point.0),
-cfg.offset.x,
);
if overflow(x - vp.x, cs.w, vp.w) > overflow(fx - vp.x, cs.w, vp.w) {
x = fx;
}
let fy = attach(
anchor.y,
anchor.h,
cs.h,
mirror(cfg.anchor_point.1),
mirror(cfg.self_point.1),
-cfg.offset.y,
);
if overflow(y - vp.y, cs.h, vp.h) > overflow(fy - vp.y, cs.h, vp.h) {
y = fy;
}
}
if cfg.fit {
x = x.min(vp.x + vp.w - cs.w).max(vp.x);
y = y.min(vp.y + vp.h - cs.h).max(vp.y);
}
Vec2::new(x, y)
}
fn positions(
tree: &mut Tree,
scroll: &mut ScrollStore,
viewport: Size,
scale: f32,
range: std::ops::Range<usize>,
) {
for i in range {
if tree.parent[i] == NIL {
tree.pos[i] = Vec2::ZERO;
}
let spec = tree.specs[i].layout;
let origin = tree.pos[i];
let size = tree.size[i];
let wrap = wraps(tree, i as u32);
let (main_content, cross_content, main_pad_start, cross_pad_start) = match spec.dir {
Dir::Row => (
size.w - spec.padding.x(),
size.h - spec.padding.y(),
spec.padding.l,
spec.padding.t,
),
Dir::Column => (
size.h - spec.padding.y(),
size.w - spec.padding.x(),
spec.padding.t,
spec.padding.l,
),
};
let mut stretch = 0.0f32;
let (total_main, max_cross) = if wrap {
let (lines, stacked, widest) = wrap_measure(tree, i as u32);
stretch = line_stretch(lines, stacked, cross_content);
(widest, stacked + stretch * lines as f32)
} else {
let mut total_main = 0.0f32;
let mut max_cross = 0.0f32;
let mut n = 0u32;
for c in tree.children(i as u32) {
if is_float(tree, c) {
continue;
}
let (c_main, c_cross) = match spec.dir {
Dir::Row => (tree.size[c as usize].w, tree.size[c as usize].h),
Dir::Column => (tree.size[c as usize].h, tree.size[c as usize].w),
};
total_main += c_main;
max_cross = max_cross.max(c_cross);
n += 1;
}
if n > 1 {
total_main += spec.gap * (n - 1) as f32;
}
(total_main, max_cross)
};
let mut offset = Vec2::ZERO;
let anchors = spec.anchor
&& !wrap
&& match spec.dir {
Dir::Row => spec.scroll_x,
Dir::Column => spec.scroll_y,
};
let free_main = (main_content - total_main).max(0.0);
let n_main = if matches!(spec.main_align, Align::Start | Align::Center | Align::End) {
0
} else {
tree.children(i as u32)
.filter(|&c| !is_float(tree, c))
.count() as u32
};
let (lead_main, between_main) = main_spread(spec.main_align, free_main, n_main);
let content_start = main_pad_start + lead_main;
if anchors && let Some((anchor, was_at)) = scroll.anchor(tree.keys[i]) {
let mut at = content_start;
let mut c = first_in_flow(tree, i as u32);
while c != NIL {
if !is_float(tree, c) {
if tree.keys[c as usize] == anchor {
let delta = at - was_at;
if delta != 0.0 {
scroll.scroll_by(
tree.keys[i],
match spec.dir {
Dir::Row => Vec2::new(delta, 0.0),
Dir::Column => Vec2::new(0.0, delta),
},
);
}
break;
}
let c_main = match spec.dir {
Dir::Row => tree.size[c as usize].w,
Dir::Column => tree.size[c as usize].h,
};
at += c_main + spec.gap + between_main;
}
c = tree.next_sibling[c as usize];
}
}
if spec.scroll_x || spec.scroll_y {
let (content_w, content_h) = match spec.dir {
Dir::Row => (total_main + spec.padding.x(), max_cross + spec.padding.y()),
Dir::Column => (max_cross + spec.padding.x(), total_main + spec.padding.y()),
};
let max = Vec2::new(
if spec.scroll_x {
(content_w - size.w).max(0.0)
} else {
0.0
},
if spec.scroll_y {
(content_h - size.h).max(0.0)
} else {
0.0
},
);
tree.scroll_max[i] = max;
offset = scroll
.resolve(
tree.keys[i],
Rect::from_pos_size(origin, size),
Size::new(content_w, content_h),
max,
tree.specs[i].transition,
)
.snapped(scale);
}
let (main_scroll, cross_scroll) = match spec.dir {
Dir::Row => (offset.x, offset.y),
Dir::Column => (offset.y, offset.x),
};
if anchors {
let mut next = None;
let mut at = content_start;
let mut c = first_in_flow(tree, i as u32);
while c != NIL {
if !is_float(tree, c) {
let c_main = match spec.dir {
Dir::Row => tree.size[c as usize].w,
Dir::Column => tree.size[c as usize].h,
};
if at + c_main > main_scroll {
next = Some((tree.keys[c as usize], at));
break;
}
at += c_main + spec.gap + between_main;
}
c = tree.next_sibling[c as usize];
}
scroll.set_anchor(tree.keys[i], next);
}
let mut c = if tree.any_float {
tree.first_child[i]
} else {
NIL
};
while c != NIL {
if let Some(cfg) = tree.specs[c as usize].layout.float {
place_float(tree, cfg, c, Rect::from_pos_size(origin, size), viewport);
}
c = tree.next_sibling[c as usize];
}
let mut cross_cursor = cross_pad_start - cross_scroll;
let mut line_start = first_in_flow(tree, i as u32);
while line_start != NIL {
let (end, extent, run_main) = if wrap {
let end = line_end(tree, line_start);
let (main, cross) = line_extents(tree, line_start, end, spec.gap);
(end, cross + stretch, main)
} else {
(NIL, cross_content, total_main)
};
let free = (main_content - run_main).max(0.0);
let (lead, between) = if wrap {
let mut n = 0u32;
let mut k = line_start;
while k != end && k != NIL {
n += u32::from(!is_float(tree, k));
k = tree.next_sibling[k as usize];
}
main_spread(spec.main_align, free, n)
} else {
(lead_main, between_main)
};
let base_above =
if tree.any_baseline && spec.cross_align == Align::Baseline && spec.dir == Dir::Row
{
Some(line_baseline(tree, line_start, end).0)
} else {
None
};
let mut cursor = main_pad_start + lead - main_scroll;
let mut c = line_start;
while c != end && c != NIL {
if is_float(tree, c) {
c = tree.next_sibling[c as usize];
continue;
}
let cs = tree.size[c as usize];
let (c_main, c_cross) = match spec.dir {
Dir::Row => (cs.w, cs.h),
Dir::Column => (cs.h, cs.w),
};
let cross_off = match base_above {
Some(above) if aligns_by_baseline(tree, c) => {
let b = first_baseline(tree, c).unwrap_or(c_cross);
cross_cursor + above - b
}
Some(_) => cross_cursor,
None => {
cross_cursor + align_factor(spec.cross_align) * (extent - c_cross).max(0.0)
}
};
tree.pos[c as usize] = match spec.dir {
Dir::Row => Vec2::new(origin.x + cursor, origin.y + cross_off),
Dir::Column => Vec2::new(origin.x + cross_off, origin.y + cursor),
};
cursor += c_main + spec.gap + between;
c = tree.next_sibling[c as usize];
}
cross_cursor += extent + spec.cross_gap;
line_start = end;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geom::Edges;
use crate::key::Key;
use crate::spec::{FloatConfig, NodeSpec};
use crate::tree::{NodeContent, OriginId, TextId};
struct StubText;
impl TextMeasure for StubText {
fn intrinsic(&mut self, id: TextId) -> Size {
Size::new(10.0 * id.0 as f32, 20.0)
}
fn wrapped(&mut self, id: TextId, max_w: f32) -> Size {
let full = 10.0 * id.0 as f32;
if max_w <= 0.0 || full <= max_w {
return Size::new(full, 20.0);
}
let lines = (full / max_w).ceil();
Size::new(max_w, lines * 20.0)
}
fn baseline(&mut self, _id: TextId) -> f32 {
15.0
}
}
struct T {
tree: Tree,
}
impl T {
fn new(root_spec: NodeSpec) -> Self {
let mut tree = Tree::new();
tree.push(
NIL,
Key::ROOT,
OriginId::HOST,
root_spec,
NodeContent::Container,
);
T { tree }
}
fn node(&mut self, parent: u32, spec: NodeSpec) -> u32 {
let key = Key::ROOT.index(self.tree.len() as u64);
self.tree
.push(parent, key, OriginId::HOST, spec, NodeContent::Container)
}
fn text(&mut self, parent: u32, chars: u32) -> u32 {
let key = Key::ROOT.index(self.tree.len() as u64);
self.tree.push(
parent,
key,
OriginId::HOST,
NodeSpec::default(),
NodeContent::Text(TextId(chars)),
)
}
fn run(&mut self, vw: f32, vh: f32) {
let mut scroll = ScrollStore::default();
compute(
&mut self.tree,
&mut StubText,
&mut scroll,
Size::new(vw, vh),
1.0,
);
}
fn size(&self, i: u32) -> Size {
self.tree.size[i as usize]
}
fn pos(&self, i: u32) -> Vec2 {
self.tree.pos[i as usize]
}
}
fn px(v: f32) -> Sizing {
Sizing::Fixed(v)
}
#[test]
fn a_grow_childs_clamp_is_its_siblings_room() {
let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
let a = t.node(0, NodeSpec::row().grow_height());
let b = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
t.run(1000.0, 1000.0);
assert_eq!(t.size(b).h, 100.0);
assert_eq!(t.size(a).h, 500.0);
let mut t = T::new(NodeSpec::row().width(px(300.0)).height(px(50.0)));
let a = t.node(0, NodeSpec::row().grow_width());
let b = t.node(0, NodeSpec::row().grow_width().min_width(200.0));
let c = t.node(0, NodeSpec::row().grow_width().max_width(20.0));
t.run(1000.0, 1000.0);
assert_eq!(t.size(b).w, 200.0);
assert_eq!(t.size(c).w, 20.0);
assert_eq!(t.size(a).w, 80.0);
assert_eq!(t.pos(c).x, 280.0);
}
#[test]
fn a_pass_freezes_only_the_violators_of_the_dominant_sign() {
let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
let a = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
let b = t.node(0, NodeSpec::row().grow_height().min_height(Min::px(500.0)));
let c = t.node(0, NodeSpec::row().grow_height());
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).h, 50.0);
assert_eq!(t.size(b).h, 500.0);
assert_eq!(t.size(c).h, 50.0);
assert_eq!(t.pos(c).y, 550.0);
let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(600.0)));
let a = t.node(0, NodeSpec::row().grow_height().max_height(100.0));
let b = t.node(0, NodeSpec::row().grow_height().min_height(Min::px(210.0)));
let c = t.node(0, NodeSpec::row().grow_height());
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).h, 100.0);
assert_eq!(t.size(b).h, 250.0);
assert_eq!(t.size(c).h, 250.0);
}
#[test]
fn fit_row_sums_children_and_gaps() {
let mut t = T::new(NodeSpec::row().pad(10.0).gap(5.0));
let r = 0;
t.node(r, NodeSpec::column().width(px(30.0)).height(px(40.0)));
t.node(r, NodeSpec::column().width(px(20.0)).height(px(25.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(r), Size::new(75.0, 60.0));
}
#[test]
fn fit_column_sums_heights() {
let mut t = T::new(NodeSpec::column().gap(4.0));
t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
t.node(0, NodeSpec::row().width(px(50.0)).height(px(10.0)));
t.node(0, NodeSpec::row().width(px(30.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(0), Size::new(50.0, 38.0));
}
#[test]
fn grow_splits_remaining_space_by_factor() {
let mut t = T::new(NodeSpec::row().width(px(300.0)).height(px(100.0)).gap(10.0));
let a = t.node(0, NodeSpec::column().width(px(50.0)).height(px(10.0)));
let b = t.node(0, NodeSpec::column().grow_width().height(px(10.0)));
let c = t.node(
0,
NodeSpec::column().width(Sizing::Grow(2.0)).height(px(10.0)),
);
t.run(1000.0, 1000.0);
let bw = t.size(b).w;
let cw = t.size(c).w;
assert!((bw - 230.0 / 3.0).abs() < 0.01, "b={bw}");
assert!((cw - 460.0 / 3.0).abs() < 0.01, "c={cw}");
assert_eq!(t.size(a).w, 50.0);
}
#[test]
fn percent_resolves_against_content_box() {
let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(100.0)).pad(10.0));
let a = t.node(
0,
NodeSpec::column()
.width(Sizing::Percent(0.5))
.height(Sizing::Percent(1.0)),
);
t.run(1000.0, 1000.0);
assert_eq!(t.size(a), Size::new(90.0, 80.0)); }
#[test]
fn cross_axis_grow_fills_content() {
let mut t = T::new(
NodeSpec::column()
.width(px(120.0))
.height(px(200.0))
.pad(8.0),
);
let a = t.node(0, NodeSpec::row().grow_width().height(px(30.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 104.0);
}
#[test]
fn nested_fit_propagates_up() {
let mut t = T::new(NodeSpec::column());
let mid = t.node(0, NodeSpec::row().pad(5.0).gap(2.0));
t.node(mid, NodeSpec::column().width(px(10.0)).height(px(10.0)));
t.node(mid, NodeSpec::column().width(px(10.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(mid), Size::new(32.0, 20.0));
assert_eq!(t.size(0), Size::new(32.0, 20.0));
}
#[test]
fn root_grow_takes_viewport() {
let mut t = T::new(NodeSpec::column().fill());
t.run(800.0, 600.0);
assert_eq!(t.size(0), Size::new(800.0, 600.0));
}
#[test]
fn positions_row_with_gap_and_padding() {
let mut t = T::new(
NodeSpec::row()
.width(px(300.0))
.height(px(100.0))
.pad(10.0)
.gap(5.0),
);
let a = t.node(0, NodeSpec::column().width(px(40.0)).height(px(20.0)));
let b = t.node(0, NodeSpec::column().width(px(40.0)).height(px(20.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(a), Vec2::new(10.0, 10.0));
assert_eq!(t.pos(b), Vec2::new(55.0, 10.0));
}
#[test]
fn main_center_alignment_offsets_children() {
let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(50.0)).center());
let a = t.node(0, NodeSpec::column().width(px(60.0)).height(px(20.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(a), Vec2::new(70.0, 15.0));
}
#[test]
fn main_end_alignment() {
let mut t = T::new(
NodeSpec::column()
.width(px(100.0))
.height(px(100.0))
.main_align(Align::End)
.gap(10.0),
);
let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
let b = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(a).y, 50.0);
assert_eq!(t.pos(b).y, 80.0);
}
fn spread(a: Align, gap: f32) -> Vec<f32> {
let mut t = T::new(
NodeSpec::row()
.width(px(100.0))
.height(px(10.0))
.gap(gap)
.main_align(a),
);
let kids: Vec<u32> = (0..3)
.map(|_| t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0))))
.collect();
t.run(1000.0, 1000.0);
kids.iter().map(|&k| t.pos(k).x).collect()
}
#[test]
fn space_between_puts_the_free_space_between_the_children() {
assert_eq!(spread(Align::SpaceBetween, 0.0), [0.0, 45.0, 90.0]);
assert_eq!(spread(Align::SpaceBetween, 10.0), [0.0, 45.0, 90.0]);
}
#[test]
fn space_around_gives_the_ends_half_a_share() {
let x = spread(Align::SpaceAround, 0.0);
let share = 70.0 / 3.0;
for (i, want) in [share / 2.0, share * 1.5 + 10.0, share * 2.5 + 20.0]
.into_iter()
.enumerate()
{
assert!((x[i] - want).abs() < 1e-3, "{x:?}");
}
}
#[test]
fn space_evenly_makes_every_gap_and_both_ends_equal() {
assert_eq!(spread(Align::SpaceEvenly, 0.0), [17.5, 45.0, 72.5]);
}
#[test]
fn a_lone_child_under_a_spread_starts_or_centres() {
for (a, want) in [
(Align::SpaceBetween, 0.0),
(Align::SpaceAround, 45.0),
(Align::SpaceEvenly, 45.0),
] {
let mut t = T::new(NodeSpec::row().width(px(100.0)).main_align(a));
let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(c).x, want, "{a:?}");
}
}
#[test]
fn a_spread_with_nothing_free_is_the_gaps_alone() {
let mut t = T::new(
NodeSpec::row()
.width(px(100.0))
.gap(5.0)
.main_align(Align::SpaceBetween),
);
let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
let b = t.node(0, NodeSpec::row().grow_width().height(px(10.0)));
let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!((t.pos(a).x, t.pos(b).x, t.pos(c).x), (0.0, 15.0, 90.0));
let mut t = T::new(
NodeSpec::row()
.width(px(100.0))
.gap(5.0)
.scroll_x()
.main_align(Align::SpaceEvenly),
);
let kids: Vec<u32> = (0..3)
.map(|_| t.node(0, NodeSpec::row().width(px(50.0)).height(px(10.0))))
.collect();
t.run(1000.0, 1000.0);
let x: Vec<f32> = kids.iter().map(|&k| t.pos(k).x).collect();
assert_eq!(x, [0.0, 55.0, 110.0]);
}
#[test]
fn a_column_spreads_its_height_and_floats_take_no_share() {
let mut t = T::new(
NodeSpec::column()
.width(px(10.0))
.height(px(100.0))
.main_align(Align::SpaceBetween),
);
let a = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
t.node(
0,
NodeSpec::row()
.width(px(5.0))
.height(px(5.0))
.float(FloatConfig::below()),
);
let b = t.node(0, NodeSpec::row().width(px(10.0)).height(px(20.0)));
t.run(1000.0, 1000.0);
assert_eq!((t.pos(a).y, t.pos(b).y), (0.0, 80.0));
}
#[test]
fn a_wrapping_row_spreads_each_line_by_itself() {
let mut t = T::new(
NodeSpec::row()
.width(px(100.0))
.wrap()
.main_align(Align::SpaceBetween),
);
let kids: Vec<u32> = (0..3)
.map(|_| t.node(0, NodeSpec::row().width(px(40.0)).height(px(10.0))))
.collect();
t.run(1000.0, 1000.0);
let at: Vec<(f32, f32)> = kids.iter().map(|&k| (t.pos(k).x, t.pos(k).y)).collect();
assert_eq!(at, [(0.0, 0.0), (60.0, 0.0), (0.0, 10.0)]);
}
#[test]
fn baseline_lines_up_text_with_a_box_s_bottom_edge() {
let mut t = T::new(NodeSpec::row().cross_align(Align::Baseline));
let txt = t.text(0, 3);
let bx = t.node(0, NodeSpec::row().width(px(10.0)).height(px(40.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(bx).y, 0.0);
assert_eq!(t.pos(txt).y, 25.0);
assert_eq!(t.size(0).h, 45.0);
}
#[test]
fn a_container_s_baseline_is_its_first_text_s() {
let mut t = T::new(NodeSpec::row().cross_align(Align::Baseline));
let bare = t.text(0, 3);
let col = t.node(
0,
NodeSpec::column().padding(Edges {
l: 0.0,
r: 0.0,
t: 10.0,
b: 0.0,
}),
);
let inner = t.text(col, 3);
t.node(col, NodeSpec::row().width(px(10.0)).height(px(30.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(col).y, 0.0);
assert_eq!(t.pos(inner).y, 10.0);
assert_eq!(t.pos(bare).y, 10.0);
assert_eq!(t.size(0).h, 60.0);
}
#[test]
fn a_grow_height_child_of_a_baseline_row_fills_from_the_top() {
let mut t = T::new(
NodeSpec::row()
.height(px(50.0))
.cross_align(Align::Baseline),
);
let txt = t.text(0, 3);
let g = t.node(0, NodeSpec::row().width(px(10.0)).grow_height());
t.run(1000.0, 1000.0);
assert_eq!((t.pos(g).y, t.size(g).h), (0.0, 50.0));
assert_eq!(t.pos(txt).y, 0.0);
}
#[test]
fn baseline_on_a_column_is_start() {
let mut t = T::new(
NodeSpec::column()
.width(px(100.0))
.cross_align(Align::Baseline),
);
let c = t.node(0, NodeSpec::row().width(px(10.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(c).x, 0.0);
}
#[test]
fn a_ratio_sizes_a_fit_height_from_the_final_width() {
let mut t = T::new(NodeSpec::column().width(px(320.0)));
let v = t.node(0, NodeSpec::column().grow_width().aspect_ratio(16.0 / 9.0));
t.node(v, NodeSpec::row().width(px(10.0)).height(px(500.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(v), Size::new(320.0, 180.0));
}
#[test]
fn a_ratio_sizes_a_fit_width_from_a_fixed_height() {
let mut t = T::new(NodeSpec::row());
let sq = t.node(0, NodeSpec::row().height(px(24.0)).aspect_ratio(1.0));
t.run(1000.0, 1000.0);
assert_eq!(t.size(sq), Size::new(24.0, 24.0));
}
#[test]
fn a_ratio_s_derived_height_is_not_shrunk() {
let mut t = T::new(NodeSpec::column().width(px(100.0)).height(px(100.0)));
let r = t.node(0, NodeSpec::column().grow_width().aspect_ratio(1.25));
let other = t.node(0, NodeSpec::column().height(Sizing::Fit));
t.node(other, NodeSpec::row().width(px(10.0)).height(px(60.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(r).h, 80.0);
assert_eq!(t.size(other).h, 60.0);
}
#[test]
fn min_fit_floors_a_ratio_height_at_its_children() {
let mut t = T::new(NodeSpec::column().width(px(100.0)));
let r = t.node(
0,
NodeSpec::column()
.grow_width()
.aspect_ratio(4.0)
.min_height(Min::FIT),
);
t.node(r, NodeSpec::row().width(px(10.0)).height(px(40.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(r).h, 40.0);
}
#[test]
fn text_gets_intrinsic_size_when_it_fits() {
let mut t = T::new(NodeSpec::column().width(px(500.0)).height(px(500.0)));
let txt = t.text(0, 8); t.run(1000.0, 1000.0);
assert_eq!(t.size(txt), Size::new(80.0, 20.0));
}
#[test]
fn text_wraps_when_clamped_by_parent() {
let mut t = T::new(
NodeSpec::column()
.width(px(100.0))
.height(px(500.0))
.pad(10.0),
);
let txt = t.text(0, 20); t.run(1000.0, 1000.0);
assert_eq!(t.size(txt).w, 80.0);
assert_eq!(t.size(txt).h, 60.0);
}
#[test]
fn text_wrapping_grows_fit_parent_height() {
let mut t = T::new(NodeSpec::column().width(px(100.0)));
let txt = t.text(0, 30); t.run(1000.0, 1000.0);
assert_eq!(t.size(txt).h, 60.0);
assert_eq!(t.size(0).h, 60.0);
}
#[test]
fn grow_with_no_space_left_gets_zero() {
let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
let a = t.node(0, NodeSpec::column().width(px(120.0)).height(px(10.0)));
let b = t.node(0, NodeSpec::column().grow_width().height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 120.0); assert_eq!(t.size(b).w, 0.0);
}
#[test]
fn deep_nesting_positions_accumulate() {
let mut t = T::new(NodeSpec::column().pad(10.0));
let l1 = t.node(0, NodeSpec::column().pad(10.0));
let l2 = t.node(l1, NodeSpec::column().pad(10.0));
let leaf = t.node(l2, NodeSpec::row().width(px(10.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.pos(leaf), Vec2::new(30.0, 30.0));
assert_eq!(t.size(0), Size::new(70.0, 70.0));
}
#[test]
fn grow_respects_max_width() {
let mut t = T::new(NodeSpec::row().width(px(800.0)).height(px(100.0)));
let a = t.node(
0,
NodeSpec::column()
.grow_width()
.max_width(560.0)
.height(px(10.0)),
);
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 560.0);
let mut t = T::new(NodeSpec::row().width(px(400.0)).height(px(100.0)));
let a = t.node(
0,
NodeSpec::column()
.grow_width()
.max_width(560.0)
.height(px(10.0)),
);
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 400.0);
}
#[test]
fn min_width_forces_fit_up() {
let mut t = T::new(NodeSpec::column());
let a = t.node(0, NodeSpec::row().min_width(120.0).height(px(10.0)));
t.node(a, NodeSpec::column().width(px(30.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 120.0);
assert_eq!(t.size(0).w, 120.0);
}
#[test]
fn percent_respects_max() {
let mut t = T::new(NodeSpec::column().width(px(1000.0)).height(px(1000.0)));
let a = t.node(
0,
NodeSpec::row()
.width(Sizing::Percent(0.9))
.max_width(300.0)
.height(px(10.0)),
);
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 300.0);
}
#[test]
fn text_rewraps_when_capped_parent_shrinks() {
let build = |parent_w: f32| {
let mut t = T::new(NodeSpec::column().width(px(parent_w)));
let txt = t.text(0, 40); t.run(1000.0, 1000.0);
t.size(txt)
};
let wide = build(500.0);
let narrow = build(100.0);
assert_eq!(wide.h, 20.0);
assert_eq!(narrow.h, 80.0); }
#[test]
fn shrink_compresses_largest_fit_child_first() {
let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
let a = t.node(0, NodeSpec::column());
t.node(a, NodeSpec::row().width(px(80.0)).height(px(10.0)));
let b = t.node(0, NodeSpec::column());
t.node(b, NodeSpec::row().width(px(40.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 60.0);
assert_eq!(t.size(b).w, 40.0);
}
#[test]
fn shrink_respects_min_and_spills_to_the_next() {
let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
let a = t.node(0, NodeSpec::column().min_width(70.0));
t.node(a, NodeSpec::row().width(px(80.0)).height(px(10.0)));
let b = t.node(0, NodeSpec::column());
t.node(b, NodeSpec::row().width(px(40.0)).height(px(10.0)));
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).w, 70.0);
assert_eq!(t.size(b).w, 30.0);
}
#[test]
fn equal_children_shrink_equally() {
let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(50.0)));
let mut kids = Vec::new();
for _ in 0..3 {
let c = t.node(0, NodeSpec::column());
t.node(c, NodeSpec::row().width(px(60.0)).height(px(10.0)));
kids.push(c);
}
t.run(1000.0, 1000.0);
for c in kids {
assert!(
(t.size(c).w - 100.0 / 3.0).abs() < 0.1,
"got {}",
t.size(c).w
);
}
}
#[test]
fn shrunk_text_rewraps() {
let mut t = T::new(NodeSpec::row().width(px(200.0)).height(px(500.0)));
t.node(0, NodeSpec::column().width(px(80.0)).height(px(10.0)));
let txt = t.text(0, 20); t.run(1000.0, 1000.0);
assert_eq!(t.size(txt).w, 120.0);
assert_eq!(t.size(txt).h, 40.0);
}
#[test]
fn text_never_shrinks_vertically() {
let mut t = T::new(NodeSpec::column().width(px(200.0)).height(px(30.0)));
let txt = t.text(0, 30); t.run(1000.0, 1000.0);
assert_eq!(
t.size(txt).h,
40.0,
"text overflows rather than clipping lines"
);
}
#[test]
fn scroll_axis_skips_shrink() {
let mut t = T::new(
NodeSpec::column()
.width(px(100.0))
.height(px(100.0))
.scroll_y(),
);
for _ in 0..2 {
let c = t.node(0, NodeSpec::column());
t.node(c, NodeSpec::row().width(px(10.0)).height(px(80.0)));
}
t.run(1000.0, 1000.0);
for c in [1u32, 3u32] {
assert_eq!(t.size(c).h, 80.0);
}
}
#[test]
fn grow_tabs_split_evenly_then_scroll_at_their_min() {
let bar = || NodeSpec::row().width(px(600.0)).height(px(30.0)).scroll_x();
let tab = || {
NodeSpec::column()
.grow_width()
.min_width(80.0)
.height(px(30.0))
};
let mut t = T::new(bar());
let tabs: Vec<u32> = (0..3).map(|_| t.node(0, tab())).collect();
t.run(1000.0, 1000.0);
for (k, &c) in tabs.iter().enumerate() {
assert_eq!(t.size(c).w, 200.0);
assert_eq!(t.pos(c).x, 200.0 * k as f32);
}
assert_eq!(t.tree.scroll_max[0].x, 0.0);
let mut t = T::new(bar());
let tabs: Vec<u32> = (0..10).map(|_| t.node(0, tab())).collect();
t.run(1000.0, 1000.0);
for (k, &c) in tabs.iter().enumerate() {
assert_eq!(t.size(c).w, 80.0);
assert_eq!(t.pos(c).x, 80.0 * k as f32);
}
assert_eq!(t.tree.scroll_max[0].x, 200.0);
let mut t = T::new(NodeSpec::row().width(px(600.0)).height(px(30.0)));
let tabs: Vec<u32> = (0..10).map(|_| t.node(0, tab())).collect();
t.run(1000.0, 1000.0);
for &c in &tabs {
assert_eq!(t.size(c).w, 80.0);
}
let mut t = T::new(bar());
let tabs: Vec<u32> = (0..10)
.map(|_| {
let c = t.node(0, tab().min_width(Min::FIT));
t.text(c, 8);
c
})
.collect();
t.run(1000.0, 1000.0);
for (k, &c) in tabs.iter().enumerate() {
assert_eq!(t.size(c).w, 80.0);
assert_eq!(t.pos(c).x, 80.0 * k as f32);
assert_eq!(t.tree.specs[c as usize].layout.min_w, Min::px(80.0));
}
assert_eq!(t.tree.scroll_max[0].x, 200.0);
let mut t = T::new(bar());
let tabs: Vec<u32> = (0..3)
.map(|_| {
let c = t.node(0, tab().min_width(Min::FIT));
t.text(c, 8);
c
})
.collect();
t.run(1000.0, 1000.0);
for &c in &tabs {
assert_eq!(t.size(c).w, 200.0);
}
}
#[test]
fn min_fit_floors_a_percent_height_at_its_content() {
let mut t = T::new(NodeSpec::row().width(px(100.0)).height(px(20.0)));
let a = t.node(
0,
NodeSpec::column()
.width(px(10.0))
.height(Sizing::Percent(0.5))
.min_height(Min::FIT),
);
t.node(a, NodeSpec::column().width(px(10.0)).height(px(16.0)));
let b = t.node(
0,
NodeSpec::column()
.width(px(10.0))
.height(Sizing::Percent(0.5))
.min_height(Min::FIT),
);
t.run(1000.0, 1000.0);
assert_eq!(t.size(a).h, 16.0);
assert_eq!(t.size(b).h, 10.0);
}
#[test]
fn padding_asymmetric() {
let mut t = T::new(
NodeSpec::column()
.width(px(100.0))
.height(px(100.0))
.padding(Edges {
l: 1.0,
r: 2.0,
t: 3.0,
b: 4.0,
}),
);
let a = t.node(0, NodeSpec::row().fill());
t.run(1000.0, 1000.0);
assert_eq!(t.pos(a), Vec2::new(1.0, 3.0));
assert_eq!(t.size(a), Size::new(97.0, 93.0));
}
}