mod anchors;
mod flex;
mod grid;
mod ir;
mod primitives;
mod text;
mod titles;
mod values;
mod wires;
pub use ir::*;
pub(crate) use text::{approx_height, approx_width};
pub(crate) use wires::cross;
pub use wires::{Rule, Severity, Violation, node_rect};
use crate::error::Error;
use crate::resolve::{Program, ResolvedInst, ResolvedValue, ShapeKind, VarTable};
use crate::span::Span;
use flex::Axis;
type GapGrowth = std::collections::BTreeMap<String, (f64, f64)>;
pub fn layout(program: &Program) -> Result<LaidOut, Error> {
layout_mode(program, true)
}
pub fn layout_raw(program: &Program) -> Result<LaidOut, Error> {
layout_mode(program, false)
}
fn layout_mode(program: &Program, growth_on: bool) -> Result<LaidOut, Error> {
let mut growth = GapGrowth::new();
let mut best = attempt(program, &growth)?;
if growth_on && !best.routing.starved.is_empty() {
let mut starved = best.routing.starved.clone();
for _ in 0..2 {
if !grow(&mut growth, &starved, program) {
break;
}
let next = attempt(program, &growth)?;
starved = next.routing.starved.clone();
if better(&next, &best) {
best = next;
}
if starved.is_empty() {
break;
}
}
}
Ok(finish(program, best))
}
struct Attempt {
nodes: Vec<PlacedNode>,
bbox: Bbox,
routing: wires::Routing,
}
fn attempt(program: &Program, growth: &GapGrowth) -> Result<Attempt, Error> {
let mut top_nodes = Vec::with_capacity(program.scene.nodes.len());
for inst in &program.scene.nodes {
top_nodes.push(layout_inst(
inst,
&program.vars,
growth,
&child_path("", inst),
)?);
}
let (bbox, _) = lay_out_container_children(
&mut top_nodes,
&program.scene.attrs,
&program.vars,
Span::empty(),
gap_bump(growth, ""),
)?;
let (scene_gap_y, _) = primitives::gap(&program.scene.attrs, &program.vars, Span::empty())?;
let (bbox, _) =
titles::place_out_bands(&mut top_nodes, bbox, scene_gap_y + gap_bump(growth, "").0)?;
let routing = wires::route_wires(program, &top_nodes)?;
Ok(Attempt {
nodes: top_nodes,
bbox,
routing,
})
}
fn better(a: &Attempt, b: &Attempt) -> bool {
let key = |t: &Attempt| {
let crossings = t
.routing
.report
.iter()
.filter(|v| v.rule == Rule::Crossing)
.count();
(t.routing.wires.len(), std::cmp::Reverse(crossings))
};
key(a) > key(b)
}
fn grow(growth: &mut GapGrowth, starved: &GapGrowth, program: &Program) -> bool {
let mut grew = false;
for (path, &(dy, dx)) in starved {
if !growable(program, path) {
continue;
}
let (gy, gx) = growth.entry(path.clone()).or_insert((0.0, 0.0));
*gy += dy;
*gx += dx;
grew |= dy > 0.0 || dx > 0.0;
}
grew
}
fn growable(program: &Program, path: &str) -> bool {
if path.is_empty() {
return true;
}
let mut nodes = &program.scene.nodes;
let mut found: Option<&ResolvedInst> = None;
for seg in path.split('.') {
match nodes.iter().find(|n| n.id.as_deref() == Some(seg)) {
Some(inst) => {
nodes = &inst.children;
found = Some(inst);
}
None => return false,
}
}
found
.is_some_and(|inst| inst.attrs.get("width").is_none() && inst.attrs.get("height").is_none())
}
fn gap_bump(growth: &GapGrowth, path: &str) -> (f64, f64) {
growth.get(path).copied().unwrap_or((0.0, 0.0))
}
fn child_path(parent: &str, inst: &ResolvedInst) -> String {
let id = inst.id.as_deref().unwrap_or("#");
if parent.is_empty() {
id.to_owned()
} else {
format!("{parent}.{id}")
}
}
fn accumulate_extent(n: &PlacedNode, ox: f64, oy: f64, bbox: &mut Bbox) {
let (wx, wy) = (ox + n.cx, oy + n.cy);
*bbox = bbox.union(n.bbox.shifted(wx, wy));
for c in &n.children {
accumulate_extent(c, wx, wy, bbox);
}
}
fn finish(program: &Program, attempt: Attempt) -> LaidOut {
let pad = values::layout_var(&program.vars, "canvas-pad").unwrap_or(20.0);
let mut bbox = attempt.bbox;
for n in &attempt.nodes {
accumulate_extent(n, 0.0, 0.0, &mut bbox);
}
let routing = attempt.routing;
let wire_points = routing.wires.iter().flat_map(|w| &w.path);
let air_points = routing.airwires.iter().flat_map(|a| [&a.from, &a.to]);
for &(x, y) in wire_points.chain(air_points) {
bbox.min_x = bbox.min_x.min(x);
bbox.min_y = bbox.min_y.min(y);
bbox.max_x = bbox.max_x.max(x);
bbox.max_y = bbox.max_y.max(y);
}
for t in routing.wires.iter().flat_map(|w| &w.texts) {
let size = t.attrs.number("font-size").unwrap_or(12.0);
let (hw, hh) = (
text::approx_width(&t.content, size) / 2.0,
text::approx_height(&t.content, size) / 2.0,
);
bbox.min_x = bbox.min_x.min(t.position.0 - hw);
bbox.min_y = bbox.min_y.min(t.position.1 - hh);
bbox.max_x = bbox.max_x.max(t.position.0 + hw);
bbox.max_y = bbox.max_y.max(t.position.1 + hh);
}
let vb = ViewBox {
x: bbox.min_x - pad,
y: bbox.min_y - pad,
w: bbox.w() + 2.0 * pad,
h: bbox.h() + 2.0 * pad,
};
let canvas_fill = program
.scene
.attrs
.get("fill")
.filter(|v| !matches!(v, ResolvedValue::Ident(s) if s == "none"))
.cloned();
LaidOut {
viewbox: vb,
nodes: attempt.nodes,
wires: routing.wires,
wire_report: routing.report,
airwires: routing.airwires,
vars: program.vars.clone(),
sheet: program.sheet.clone(),
canvas_fill,
}
}
pub fn validate_routing(laid: &LaidOut) -> Vec<Violation> {
let mut out = laid.wire_report.clone();
out.extend(wires::validate_routing(
&laid.nodes,
&laid.wires,
&laid.wire_report,
&laid.vars,
));
out
}
fn layout_inst(
inst: &ResolvedInst,
vars: &VarTable,
growth: &GapGrowth,
path: &str,
) -> Result<PlacedNode, Error> {
let mut children: Vec<PlacedNode> = Vec::with_capacity(inst.children.len());
for c in &inst.children {
children.push(layout_inst(c, vars, growth, &child_path(path, c))?);
}
let mut dividers: Vec<GridRule> = Vec::new();
let mut frame: Option<Bbox> = None;
let bbox = if children.is_empty() {
primitives::leaf_bbox(inst, vars)?
} else {
let (content_bbox, rules) = lay_out_container_children(
&mut children,
&inst.attrs,
vars,
inst.span,
gap_bump(growth, path),
)?;
dividers = rules;
let b = primitives::closed_bbox(inst, content_bbox, vars)?;
let text_only = children.iter().all(|c| c.shape == ShapeKind::Text);
const CLOUD_LABEL_DROP: f64 = 0.1;
const CYL_LABEL_DROP: f64 = 0.03;
let label_drop = match inst.shape {
ShapeKind::Cloud => CLOUD_LABEL_DROP,
ShapeKind::Cyl => CYL_LABEL_DROP,
_ => 0.0,
};
if label_drop > 0.0 && text_only {
let dy = b.h() * label_drop;
for c in &mut children {
c.cy += dy;
}
}
let (gap_y, _) = primitives::gap(&inst.attrs, vars, inst.span)?;
let (footprint, has_out) =
titles::place_out_bands(&mut children, b, gap_y + gap_bump(growth, path).0)?;
if has_out {
frame = Some(b);
}
footprint
};
let rotation = inst.attrs.number("rotate").unwrap_or(0.0);
Ok(PlacedNode {
id: inst.id.clone(),
shape: inst.shape,
type_chain: inst.type_chain.clone(),
applied_styles: inst.applied_styles.clone(),
label: inst.label.clone(),
attrs: inst.attrs.clone(),
markers: inst.markers.clone(),
cx: 0.0,
cy: 0.0,
bbox,
frame,
rotation,
children,
dividers,
span: inst.span,
})
}
fn one_d_dividers(
children: &[PlacedNode],
flow: &[usize],
mode: LayoutMode,
flow_bbox: Bbox,
) -> Vec<GridRule> {
let row = matches!(mode, LayoutMode::Row);
let main = |i: usize| if row { children[i].cx } else { children[i].cy };
let half = |i: usize| {
if row {
children[i].bbox.w() / 2.0
} else {
children[i].bbox.h() / 2.0
}
};
let mut order: Vec<usize> = flow.to_vec();
order.sort_by(|&a, &b| main(a).total_cmp(&main(b)));
let mut segs = Vec::new();
for pair in order.windows(2) {
let mid = (main(pair[0]) + half(pair[0]) + main(pair[1]) - half(pair[1])) / 2.0;
if row {
segs.push((mid, flow_bbox.min_y, mid, flow_bbox.max_y));
} else {
segs.push((flow_bbox.min_x, mid, flow_bbox.max_x, mid));
}
}
segs
}
fn lay_out_container_children(
children: &mut [PlacedNode],
container_attrs: &crate::resolve::AttrMap,
vars: &VarTable,
span: Span,
grow: (f64, f64),
) -> Result<(Bbox, Vec<GridRule>), Error> {
if children.is_empty() {
return Ok((Bbox::empty(), Vec::new()));
}
let grown;
let container_attrs = if grow == (0.0, 0.0) {
container_attrs
} else {
let (gy, gx) = primitives::gap(container_attrs, vars, span)?;
let mut attrs = container_attrs.clone();
attrs.insert(
"gap",
ResolvedValue::Tuple(vec![
ResolvedValue::Number(gy + grow.0),
ResolvedValue::Number(gx + grow.1),
]),
);
grown = attrs;
&grown
};
let margins: Vec<(f64, f64, f64, f64)> = children
.iter()
.map(|c| primitives::margin(&c.attrs, c.span))
.collect::<Result<_, _>>()?;
for (c, &(t, r, b, l)) in children.iter_mut().zip(&margins) {
c.bbox = c.bbox.expand(t, r, b, l);
}
let mut flow_indices: Vec<usize> = Vec::new();
let mut abs_indices: Vec<usize> = Vec::new();
let mut reserve_indices: Vec<usize> = Vec::new();
for (i, c) in children.iter().enumerate() {
match anchors::child_role(&c.attrs, c.span)? {
anchors::Role::Flow => flow_indices.push(i),
anchors::Role::Reserve => reserve_indices.push(i),
anchors::Role::Absolute => abs_indices.push(i),
}
}
let mode = read_layout_mode(container_attrs, span)?;
let pad = primitives::padding(container_attrs, vars, span)?;
let avail = (
container_attrs
.number("width")
.map(|w| (w - pad.left - pad.right).max(0.0)),
container_attrs
.number("height")
.map(|h| (h - pad.top - pad.bottom).max(0.0)),
);
let mut grid_rules: Vec<GridRule> = Vec::new();
let flow_bbox = if !flow_indices.is_empty() {
let mut flow_children: Vec<PlacedNode> =
flow_indices.iter().map(|i| children[*i].clone()).collect();
let bbox = match mode {
LayoutMode::Row => flex::lay_out_flex(
Axis::Row,
&mut flow_children,
container_attrs,
vars,
span,
avail,
)?,
LayoutMode::Column => flex::lay_out_flex(
Axis::Column,
&mut flow_children,
container_attrs,
vars,
span,
avail,
)?,
LayoutMode::Grid => {
if grid::is_inset_grid(container_attrs) {
for c in &mut flow_children {
c.bbox = c.bbox.expand(pad.top, pad.right, pad.bottom, pad.left);
}
}
let (bbox, rules) =
grid::lay_out_grid(&mut flow_children, container_attrs, vars, span)?;
grid_rules = rules;
bbox
}
};
for (slot, placed) in flow_indices.iter().zip(flow_children) {
children[*slot] = placed;
}
bbox
} else {
Bbox::empty()
};
if matches!(mode, LayoutMode::Row | LayoutMode::Column)
&& grid::read_divider(container_attrs) != grid::Divider::None
&& flow_indices.len() > 1
{
grid_rules = one_d_dividers(children, &flow_indices, mode, flow_bbox);
}
let in_indices: Vec<usize> = reserve_indices
.iter()
.copied()
.filter(|&i| !anchors::is_out_band(&children[i].attrs))
.collect();
let body_bbox = if in_indices.is_empty() {
flow_bbox
} else {
let (gap_y, _) = primitives::gap(container_attrs, vars, span)?;
titles::reserve_bands(
children,
&flow_indices,
&in_indices,
flow_bbox,
&mut grid_rules,
gap_y,
)
};
let anchor_parent_bbox = container_anchor_bbox(container_attrs).unwrap_or(body_bbox);
for i in &abs_indices {
let pos = anchors::read_pos(&children[*i].attrs, children[*i].span)?
.expect("abs child carries at: or side:");
let offset = match children[*i].attrs.get("offset") {
Some(v) => anchors::parse_offset(v, children[*i].span)?,
None => (0.0, 0.0),
};
let (target_cx, target_cy) = anchors::resolve(pos, anchor_parent_bbox, children[*i].bbox);
let cb = children[*i].bbox;
let local_off_x = (cb.min_x + cb.max_x) / 2.0;
let local_off_y = (cb.min_y + cb.max_y) / 2.0;
children[*i].cx = target_cx + offset.0 - local_off_x;
children[*i].cy = target_cy + offset.1 - local_off_y;
}
for (c, &(t, r, b, l)) in children.iter_mut().zip(&margins) {
c.bbox = c.bbox.expand(-t, -r, -b, -l);
}
Ok((body_bbox, grid_rules))
}
#[derive(Clone, Copy, Debug)]
enum LayoutMode {
Row,
Column,
Grid,
}
fn read_layout_mode(attrs: &crate::resolve::AttrMap, span: Span) -> Result<LayoutMode, Error> {
match attrs.get("layout") {
None => Ok(LayoutMode::Column),
Some(ResolvedValue::Ident(s)) => match s.as_str() {
"row" => Ok(LayoutMode::Row),
"column" => Ok(LayoutMode::Column),
"grid" => Ok(LayoutMode::Grid),
other => Err(Error::at(
span,
format!("unknown layout '{}' — expected row, column, or grid", other),
)),
},
Some(_) => Err(Error::at(span, "'layout' expects row, column, or grid")),
}
}
fn container_anchor_bbox(attrs: &crate::resolve::AttrMap) -> Option<Bbox> {
let w = attrs.number("width")?;
let h = attrs.number("height")?;
Some(Bbox::centered(w, h))
}
#[cfg(test)]
mod tests {
use super::*;
fn lay_out(src: &str) -> LaidOut {
let tokens = crate::lexer::lex(src).expect("lex");
let file = crate::syntax::parser::parse(&tokens).expect("parse");
let program = crate::resolve::resolve_with_theme(&file, &[]).expect("resolve");
layout(&program).expect("layout")
}
#[test]
fn empty_closed_shape_is_two_paddings() {
let n = &lay_out("|box|\n").nodes[0];
assert!((n.bbox.w() - 33.0).abs() < 0.01, "w={}", n.bbox.w());
assert!((n.bbox.h() - 33.0).abs() < 0.01, "h={}", n.bbox.h());
}
#[test]
fn explicit_dims_are_border_box() {
let n = &lay_out("|box| { width: 100; height: 50; }\n").nodes[0];
assert!((n.bbox.w() - 101.0).abs() < 0.01, "w={}", n.bbox.w());
assert!((n.bbox.h() - 51.0).abs() < 0.01, "h={}", n.bbox.h());
}
#[test]
fn stroke_width_counts_toward_the_bbox() {
let n = &lay_out("|box| { width: 100; height: 50; stroke-width: 4; }\n").nodes[0];
assert!((n.bbox.w() - 104.0).abs() < 0.01, "w={}", n.bbox.w());
assert!((n.bbox.h() - 54.0).abs() < 0.01, "h={}", n.bbox.h());
}
#[test]
fn label_auto_sizes_to_content_plus_padding() {
let n = &lay_out("|box| { \"hi\" }\n").nodes[0];
assert!(n.bbox.w() > 40.0 && n.bbox.w() < 60.0, "w={}", n.bbox.w());
}
#[test]
fn dims_are_independent_per_axis() {
let n = &lay_out("|box| { width: 200; \"hi\" }\n").nodes[0];
assert!((n.bbox.w() - 201.0).abs() < 0.01, "w={}", n.bbox.w());
assert!((n.bbox.h() - 47.0).abs() < 0.01, "h={}", n.bbox.h());
}
#[test]
fn oval_uses_width_height() {
let n = &lay_out("|oval| { width: 100; height: 50; }\n").nodes[0];
assert!((n.bbox.w() - 101.0).abs() < 0.01, "w={}", n.bbox.w());
assert!((n.bbox.h() - 51.0).abs() < 0.01, "h={}", n.bbox.h());
}
#[test]
fn text_sizes_to_its_glyphs_without_padding() {
let n = &lay_out("\"hi\"\n").nodes[0];
assert!((n.bbox.w() - 15.4).abs() < 0.5, "w={}", n.bbox.w()); assert!((n.bbox.h() - 14.0).abs() < 0.5, "h={}", n.bbox.h());
}
#[test]
fn row_layout_stacks_horizontally() {
let l = lay_out(
"layout: row; gap: 10;\n\
|box| { width: 100; height: 40; }\n\
|box| { width: 60; height: 40; }\n",
);
assert_eq!(l.nodes.len(), 2);
let dx = l.nodes[1].cx - l.nodes[0].cx;
assert!((dx - 91.0).abs() < 0.5, "dx={}", dx);
assert!((l.nodes[0].cy - l.nodes[1].cy).abs() < 0.01);
}
#[test]
fn column_layout_stacks_vertically() {
let l = lay_out(
"layout: column; gap: 20;\n\
|box| { width: 100; height: 40; }\n\
|box| { width: 100; height: 60; }\n",
);
let dy = l.nodes[1].cy - l.nodes[0].cy;
assert!((dy - 71.0).abs() < 0.5, "dy={}", dy);
assert!((l.nodes[0].cx - l.nodes[1].cx).abs() < 0.01);
}
#[test]
fn viewbox_wraps_content_with_canvas_pad() {
let l = lay_out("|box| { width: 100; height: 40; }\n");
assert!((l.viewbox.w - 141.0).abs() < 0.01, "w={}", l.viewbox.w);
assert!((l.viewbox.h - 81.0).abs() < 0.01, "h={}", l.viewbox.h);
}
#[test]
fn group_caption_reserves_a_band_above_the_content() {
let h = |src: &str| lay_out(src).nodes[0].bbox.h();
let plain = h("g |group| {\n a |box| { width: 80; height: 30; }\n}\n");
let capped =
h("g |group| {\n |caption| { \"Cap\" }\n a |box| { width: 80; height: 30; }\n}\n");
assert!(
capped > plain + 10.0,
"caption adds a band: plain={plain} capped={capped}"
);
}
#[test]
fn caption_sits_above_the_content() {
let l = lay_out(
"g |group| {\n |caption| { \"Cap\" }\n a |box| { width: 80; height: 30; }\n}\n",
);
let g = &l.nodes[0];
let cap = g
.children
.iter()
.find(|c| c.type_chain.iter().any(|t| t == "caption"))
.expect("caption child");
let a = g
.children
.iter()
.find(|c| c.id.as_deref() == Some("a"))
.expect("box child");
assert!(cap.cy < a.cy, "cap.cy={} a.cy={}", cap.cy, a.cy);
}
#[test]
fn justify_orders_children_start_center_end() {
let first_cx = |j: &str| {
let src = format!(
"g |row| {{ width: 300; justify: {j};\n a |box| {{ width: 40; height: 20; }}\n b |box| {{ width: 40; height: 20; }}\n}}\n"
);
lay_out(&src).nodes[0].children[0].cx
};
let (start, center, end) = (first_cx("start"), first_cx("center"), first_cx("end"));
assert!(
start < center && center < end,
"start={start} center={center} end={end}"
);
}
#[test]
fn justify_evenly_spaces_children_equally() {
let l = lay_out(
"g |row| { width: 300; justify: evenly;\n a |box| { width: 20; height: 20; }\n b |box| { width: 20; height: 20; }\n c |box| { width: 20; height: 20; }\n}\n",
);
let cx: Vec<f64> = l.nodes[0].children.iter().map(|c| c.cx).collect();
assert!(
((cx[1] - cx[0]) - (cx[2] - cx[1])).abs() < 0.01,
"centers {cx:?}"
);
}
#[test]
fn align_stretch_fills_the_cross_axis() {
let l = lay_out("g |row| { height: 80; align: stretch;\n a |box| { width: 40; }\n}\n");
let a = &l.nodes[0].children[0];
assert!((a.bbox.h() - 80.0).abs() < 1.0, "a.h={}", a.bbox.h());
}
#[test]
fn no_slack_means_no_distribution() {
let span = |j: &str| {
let src = format!(
"g |row| {{ justify: {j};\n a |box| {{ width: 40; height: 20; }}\n b |box| {{ width: 40; height: 20; }}\n}}\n"
);
let l = lay_out(&src);
l.nodes[0].children[1].cx - l.nodes[0].children[0].cx
};
assert!(
(span("start") - span("end")).abs() < 0.01,
"auto row: justify is a no-op"
);
}
#[test]
fn grid_fixed_columns_place_children_in_order() {
let l = lay_out(
"layout: grid; columns: 80 80 80; gap: 0;\n\
a |box| { width: 40; height: 40; }\n\
b |box| { width: 40; height: 40; }\n\
c |box| { width: 40; height: 40; }\n",
);
let cx: Vec<f64> = l.nodes.iter().map(|n| n.cx).collect();
assert!((cx[1] - cx[0] - 80.0).abs() < 0.5, "dx={}", cx[1] - cx[0]);
assert!((cx[2] - cx[1] - 80.0).abs() < 0.5);
assert!((l.nodes[0].cy - l.nodes[1].cy).abs() < 0.01);
}
#[test]
fn grid_repeat_makes_auto_columns_and_wraps() {
let l = lay_out(
"layout: grid; columns: repeat(2);\n\
a |box| { width: 30; height: 30; }\n\
b |box| { width: 30; height: 30; }\n\
c |box| { width: 30; height: 30; }\n",
);
assert!(l.nodes[2].cy > l.nodes[0].cy, "c below a");
}
#[test]
fn grid_cell_pins_placement() {
let l = lay_out(
"layout: grid; columns: repeat(3);\n\
a |box| { cell: 3 1; }\n\
b |box|\n",
);
assert!(
l.nodes[0].cx > l.nodes[1].cx,
"a (col 3) right of b (col 1)"
);
}
#[test]
fn grid_cell_fills_its_track_under_stretch() {
let l = lay_out(
"layout: grid; columns: 120 120; gap: 0;\n\
a |box| { justify: stretch; align: stretch; }\n\
b |box|\n",
);
assert!(
(l.nodes[0].bbox.w() - 120.0).abs() < 1.0,
"a.w={}",
l.nodes[0].bbox.w()
);
}
#[test]
fn grid_rows_track_list_is_a_floor_implicit_rows_overflow() {
let l = lay_out(
"layout: grid; columns: 40 40; rows: auto;\n\
a |box| { width: 30; height: 30; }\n\
b |box| { width: 30; height: 30; }\n\
c |box| { width: 30; height: 30; }\n\
d |box| { width: 30; height: 30; }\n",
);
assert!(l.nodes[2].cy > l.nodes[0].cy, "c (row 2) below a (row 1)");
assert!(
(l.nodes[2].cy - l.nodes[3].cy).abs() < 0.01,
"c, d share row 2"
);
}
#[test]
fn grid_without_columns_is_an_error() {
let tokens = crate::lexer::lex("layout: grid;\na |box|\nb |box|\n").expect("lex");
let file = crate::syntax::parser::parse(&tokens).expect("parse");
let program = crate::resolve::resolve_with_theme(&file, &[]).expect("resolve");
assert!(layout(&program).is_err());
}
#[test]
fn table_draws_interior_dividers_no_frame() {
let l = lay_out("t |table| { columns: 40 40;\n \"a\" \"b\" \"c\" \"d\"\n}\n");
assert!(
!l.nodes[0].dividers.is_empty(),
"table has interior dividers"
);
assert!(
lay_out("g |group| { x |box| }\n").nodes[0]
.dividers
.is_empty()
);
}
#[test]
fn grid_dividers_stay_within_the_content_box() {
let l = lay_out(
"t |table| { columns: 40 40; gap: 20;\n \"a\"\n \"b\"\n \"c\"\n \"d\"\n}\n",
);
let t = &l.nodes[0];
let (hw, hh) = (t.draw_box().w() / 2.0 + 0.01, t.draw_box().h() / 2.0 + 0.01);
for (x1, y1, x2, y2) in &t.dividers {
for (x, y) in [(x1, y1), (x2, y2)] {
assert!(x.abs() <= hw, "divider x {x} exceeds half-width {hw}");
assert!(y.abs() <= hh, "divider y {y} exceeds half-height {hh}");
}
}
}
#[test]
fn one_d_divider_falls_between_flow_children() {
let l = lay_out(
"g |row| { divider: all;\n a |box| { width: 30; height: 30; }\n b |box| { width: 30; height: 30; }\n c |box| { width: 30; height: 30; }\n}\n",
);
assert_eq!(
l.nodes[0].dividers.len(),
2,
"two separators between three children"
);
}
}