mod common;
use common::*;
use zenith_core::default_provider;
use zenith_scene::compile;
use zenith_scene::ir::SceneCommand;
#[test]
fn text_wraps_when_exceeding_box_width() {
let runs = wrap_runs(
10.0,
120.0,
"start",
"the quick brown fox jumps over the lazy dog",
);
assert!(
runs.len() > 1,
"wrapped text must emit more than one run; got {}",
runs.len()
);
let mut ys: Vec<f64> = runs.iter().map(|(_, y)| *y).collect();
ys.sort_by(|a, b| a.partial_cmp(b).unwrap());
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
assert!(
ys.len() >= 2,
"wrapped text must occupy >= 2 distinct baselines; got {ys:?}"
);
}
#[test]
fn text_fits_single_line_unchanged() {
let runs = wrap_runs(40.0, 600.0, "start", "Hi there");
let y0 = runs[0].1;
assert!(
runs.iter().all(|(_, y)| (*y - y0).abs() < 1e-6),
"fitting text must stay on one line; got {runs:?}"
);
assert_eq!(
runs[0].0, 40.0,
"start-aligned fitting text must begin at node x"
);
}
#[test]
fn text_wrap_center_lines_inset() {
let runs = wrap_runs(
10.0,
120.0,
"center",
"the quick brown fox jumps over the lazy dog",
);
assert!(runs.len() > 1, "expected wrapping; got {}", runs.len());
let mut seen_y: Vec<f64> = Vec::new();
for (x, y) in &runs {
if !seen_y.iter().any(|sy| (*sy - *y).abs() < 1e-6) {
seen_y.push(*y);
assert!(
*x > 10.0,
"center-wrapped line first run x ({x}) must be inset past node x (10)"
);
}
}
}
#[test]
fn text_wrap_justify_spreads() {
let node_x = 10.0;
let box_w = 120.0;
let src = format!(
r##"zenith version=1 {{
project id="proj.wj" name="WJ"
tokens format="zenith-token-v1" {{}}
styles {{}}
document id="doc.wj" title="WJ" {{
page id="page.wj" w=(px)1000 h=(px)600 {{
text id="text.wj" x=(px){node_x} y=(px)20 w=(px){box_w} align="justify" {{
span "the quick brown fox jumps over the lazy dog"
}}
}}
}}
}}
"##
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let runs: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| {
if let SceneCommand::DrawGlyphRun { x, y, .. } = c {
Some((*y, *x))
} else {
None
}
})
.collect();
assert!(runs.len() > 1, "expected wrapping; got {}", runs.len());
let mut ys: Vec<f64> = Vec::new();
for (y, _) in &runs {
if !ys.iter().any(|v| (*v - *y).abs() < 1e-6) {
ys.push(*y);
}
}
assert!(ys.len() >= 2, "need >= 2 lines; got {}", ys.len());
let first_line_y = ys[0];
let first_line_first_x = runs
.iter()
.filter(|(y, _)| (*y - first_line_y).abs() < 1e-6)
.map(|(_, x)| *x)
.fold(f64::INFINITY, f64::min);
assert!(
(first_line_first_x - node_x).abs() < 1e-6,
"justified first line must start at node x; got {first_line_first_x}"
);
let last_line_y = ys[ys.len() - 1];
let last_line_first_x = runs
.iter()
.filter(|(y, _)| (*y - last_line_y).abs() < 1e-6)
.map(|(_, x)| *x)
.fold(f64::INFINITY, f64::min);
assert!(
(last_line_first_x - node_x).abs() < 1e-6,
"last (start-aligned) line must begin at node x; got {last_line_first_x}"
);
}
#[test]
fn text_wrap_justify_fills_box_width() {
let node_x = 10.0;
let box_w = 120.0;
let src = format!(
r##"zenith version=1 {{
project id="proj.jf" name="JF"
tokens format="zenith-token-v1" {{}}
styles {{}}
document id="doc.jf" title="JF" {{
page id="page.jf" w=(px)1000 h=(px)600 {{
text id="text.jf" x=(px){node_x} y=(px)20 w=(px){box_w} align="justify" {{
span "the quick brown fox jumps over the lazy dog"
}}
}}
}}
}}
"##
);
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let runs: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::DrawGlyphRun { x, y, .. } => Some((*y, *x)),
_ => None,
})
.collect();
let mut ys: Vec<f64> = Vec::new();
for (y, _) in &runs {
if !ys.iter().any(|v| (*v - *y).abs() < 1e-6) {
ys.push(*y);
}
}
assert!(ys.len() >= 2, "need >= 2 lines; got {}", ys.len());
let first_y = ys[0];
let max_x_first = runs
.iter()
.filter(|(y, _)| (*y - first_y).abs() < 1e-6)
.map(|(_, x)| *x)
.fold(f64::NEG_INFINITY, f64::max);
let box_right = node_x + box_w;
let box_mid = node_x + box_w / 2.0;
assert!(
max_x_first > box_mid,
"justified line's last word must be pushed past box midpoint {box_mid}; got {max_x_first} (box_right={box_right})"
);
}
#[test]
fn text_node_unregistered_family_falls_back_and_emits_advisory() {
let src = r##"zenith version=1 {
project id="proj.fb1" name="FB1"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.fb1" title="FB1" {
page id="page.fb1" w=(px)400 h=(px)200 {
text id="headline" x=(px)10 y=(px)10 font-family="Oswald" {
span "Hello"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
assert!(
result
.scene
.commands
.iter()
.any(|c| matches!(c, SceneCommand::DrawGlyphRun { .. })),
"expected DrawGlyphRun when unregistered family falls back; commands: {:?}",
result.scene.commands,
);
let unresolved: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == "font.unresolved")
.collect();
assert_eq!(
unresolved.len(),
1,
"expected exactly one font.unresolved diagnostic, got {:?}",
unresolved,
);
let msg = &unresolved[0].message;
assert!(
msg.contains("headline"),
"advisory message should name the node 'headline'; got: {msg}"
);
assert!(
msg.contains("Oswald"),
"advisory message should name the missing family 'Oswald'; got: {msg}"
);
}
#[test]
fn text_node_registered_family_no_advisory() {
let src = r##"zenith version=1 {
project id="proj.fb2" name="FB2"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.fb2" title="FB2" {
page id="page.fb2" w=(px)400 h=(px)200 {
text id="body.text" x=(px)10 y=(px)10 font-family="Noto Sans" {
span "Hello"
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
let unresolved: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == "font.unresolved")
.collect();
assert!(
unresolved.is_empty(),
"expected no font.unresolved diagnostics for registered family; got: {:?}",
unresolved,
);
assert!(
result
.scene
.commands
.iter()
.any(|c| matches!(c, SceneCommand::DrawGlyphRun { .. })),
"expected DrawGlyphRun for registered Noto Sans family",
);
}