mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-29 04:47:10 -06:00
473 lines
19 KiB
Rust
473 lines
19 KiB
Rust
use super::*;
|
|
use wordcraft_doc::para::InlineObject;
|
|
use wordcraft_doc::props::{Align, ParaProps};
|
|
use wordcraft_doc::{Pos, Table};
|
|
|
|
fn lay(doc: &Document) -> DocLayout {
|
|
let mut c = LayoutCache::new();
|
|
layout(doc, &mut c, &LayoutOptions::default())
|
|
}
|
|
|
|
fn lines_of(l: &DocLayout) -> usize {
|
|
l.pages.iter().flat_map(|p| p.items.iter()).map(|it| if let Placed::Lines { l0, l1, .. } = it { l1 - l0 } else { 0 }).sum()
|
|
}
|
|
|
|
#[test]
|
|
fn empty_doc_has_one_page() {
|
|
let d = Document::new();
|
|
let l = lay(&d);
|
|
assert_eq!(l.pages.len(), 1);
|
|
assert_eq!(lines_of(&l), 1);
|
|
let c = l.caret(&Pos::body(0, 0)).unwrap();
|
|
assert_eq!(c.page, 0);
|
|
assert!((c.x - 72.0).abs() < 0.5, "{c:?}");
|
|
assert!(c.top >= 72.0 && c.height > 8.0);
|
|
}
|
|
|
|
#[test]
|
|
fn long_paragraph_wraps_and_paginates() {
|
|
let text = "The quick brown fox jumps over the lazy dog. ".repeat(400);
|
|
let d = Document::from_text(&text);
|
|
let l = lay(&d);
|
|
assert!(l.pages.len() >= 3, "pages {}", l.pages.len());
|
|
// Every line fits the text width.
|
|
for p in &l.pages {
|
|
for it in &p.items {
|
|
if let Placed::Lines { para, l0, l1, x, .. } = it {
|
|
for li in *l0..*l1 {
|
|
let line = ¶.lines[li];
|
|
let w = para.x_of(li, line.stop).unwrap() + x;
|
|
assert!(w <= 612.0 - 72.0 + 30.0, "line too wide: {w}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hit_and_caret_agree() {
|
|
let d = Document::from_text("Hello world, this is WordCraft.\nSecond paragraph here.");
|
|
let l = lay(&d);
|
|
for off in [0, 5, 12, 31] {
|
|
let pos = Pos::body(0, off);
|
|
let c = l.caret(&pos).unwrap();
|
|
let back = l.hit(c.page, c.x + 0.1, c.top + c.height / 2.0, StoryRef::Body).unwrap();
|
|
assert_eq!(back, pos, "off {off}");
|
|
}
|
|
let c1 = l.caret(&Pos::body(1, 0)).unwrap();
|
|
let (down, _) = l.vertical(&Pos::body(0, 0), None, 1, 0).unwrap();
|
|
assert_eq!(down.path, Path::top(1));
|
|
assert!(c1.top > l.caret(&Pos::body(0, 0)).unwrap().top);
|
|
let (up, _) = l.vertical(&Pos::body(1, 3), None, -1, 0).unwrap();
|
|
assert_eq!(up.path, Path::top(0));
|
|
}
|
|
|
|
#[test]
|
|
fn page_break_char_starts_new_page() {
|
|
let mut d = Document::from_text("one");
|
|
d.insert_text(&Pos::body(0, 3), "\u{000C}", &Default::default()).unwrap();
|
|
let l = lay(&d);
|
|
assert_eq!(l.pages.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn center_and_right_alignment() {
|
|
let mut d = Document::from_text("abc\nabc\nabc");
|
|
d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 0), &|p| p.align = Some(Align::Center)).unwrap();
|
|
d.format_paragraphs(&Pos::body(2, 0), &Pos::body(2, 0), &|p| p.align = Some(Align::Right)).unwrap();
|
|
let l = lay(&d);
|
|
let x0 = l.caret(&Pos::body(0, 0)).unwrap().x;
|
|
let x1 = l.caret(&Pos::body(1, 0)).unwrap().x;
|
|
let x2 = l.caret(&Pos::body(2, 3)).unwrap().x;
|
|
assert!((x0 - 72.0).abs() < 0.5);
|
|
assert!(x1 > 250.0 && x1 < 320.0, "{x1}");
|
|
assert!((x2 - 540.0).abs() < 1.0, "{x2}");
|
|
}
|
|
|
|
#[test]
|
|
fn tabs_land_on_default_stops() {
|
|
let d = Document::from_text("a\tb");
|
|
let l = lay(&d);
|
|
let xb = l.caret(&Pos::body(0, 2)).unwrap().x;
|
|
assert!((xb - (72.0 + 36.0)).abs() < 0.5, "{xb}");
|
|
}
|
|
|
|
#[test]
|
|
fn right_tab_aligns_text_end() {
|
|
let mut d = Document::from_text("left\tright");
|
|
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| {
|
|
p.tabs = Some(vec![wordcraft_doc::props::TabStop { pos: 400.0, align: wordcraft_doc::props::TabAlign::Right, leader: Default::default() }])
|
|
})
|
|
.unwrap();
|
|
let l = lay(&d);
|
|
let end = l.caret(&Pos::body(0, 10)).unwrap().x;
|
|
assert!((end - 472.0).abs() < 1.0, "{end}");
|
|
}
|
|
|
|
#[test]
|
|
fn lists_get_labels_and_indent() {
|
|
let mut d = Document::from_text("one\ntwo\nthree");
|
|
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
|
|
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(2, 0), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap();
|
|
let l = lay(&d);
|
|
let labels: Vec<String> = l.pages[0]
|
|
.items
|
|
.iter()
|
|
.filter_map(|it| if let Placed::Lines { para, .. } = it { para.label.as_ref().map(|x| x.text.clone()) } else { None })
|
|
.collect();
|
|
assert_eq!(labels, vec!["1.", "2.", "3."]);
|
|
let x = l.caret(&Pos::body(0, 0)).unwrap().x;
|
|
assert!((x - 108.0).abs() < 0.5, "{x}");
|
|
}
|
|
|
|
#[test]
|
|
fn symbol_font_bullet_without_the_font_draws_a_bullet() {
|
|
// Word's default bullet: U+F0B7 in Symbol.
|
|
let mut d = Document::from_text("one");
|
|
let id = d.numbering.add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
|
|
for a in &mut d.numbering.abstracts {
|
|
if let Some(l) = a.levels.first_mut() {
|
|
l.chr.font = Some("Symbol".into());
|
|
}
|
|
}
|
|
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap();
|
|
let l = lay(&d);
|
|
let label = l.pages[0].items.iter().find_map(|it| if let Placed::Lines { para, .. } = it { para.label.clone() } else { None }).unwrap();
|
|
// Where a real Symbol font with the private-use code is installed (Windows), the code stays.
|
|
let symbol = wordcraft_fonts::word::resolve("Symbol", false, false);
|
|
let expected = if symbol.substituted || !symbol.face.covers('\u{F0B7}') { "\u{2022}" } else { "\u{F0B7}" };
|
|
assert_eq!(label.text, expected);
|
|
assert_eq!(label.glyphs.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn tables_lay_out_cells() {
|
|
let mut d = Document::from_text("before\nafter");
|
|
let mut t = Table::new(2, 3, 468.0);
|
|
t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("cell text", Default::default()))];
|
|
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
|
let l = lay(&d);
|
|
let pos = Pos { story: StoryRef::Body, path: Path(vec![1, 0, 1, 0]), off: 0 };
|
|
let c = l.caret(&pos).unwrap();
|
|
assert!(c.x > 72.0 + 150.0 && c.x < 72.0 + 170.0, "{c:?}");
|
|
let after = l.caret(&Pos::body(2, 0)).unwrap();
|
|
assert!(after.top > c.top + 20.0);
|
|
let rules = l.pages[0].items.iter().filter(|i| matches!(i, Placed::Rule { .. })).count();
|
|
assert!(rules >= 12, "rules {rules}");
|
|
assert!(l.cell_at(0, c.x, c.top + 2.0).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn headers_and_page_fields() {
|
|
let mut d = Document::from_text(&"para\n".repeat(120));
|
|
let mut hp = wordcraft_doc::Paragraph::new();
|
|
hp.insert_object(0, InlineObject::Field { instr: "PAGE".into(), result: String::new(), locked: false }, &Default::default()).unwrap();
|
|
let id = d.add_part(wordcraft_doc::PartKind::Footer, vec![wordcraft_doc::para_block(hp)]);
|
|
d.last_section.footers.default = Some(id);
|
|
let l = lay(&d);
|
|
assert!(l.pages.len() >= 2);
|
|
for (i, p) in l.pages.iter().enumerate() {
|
|
assert_eq!(p.footer_story, Some(id));
|
|
let label = p.footer.iter().find_map(|it| if let Placed::Lines { para, .. } = it { Some(para.clusters.len()) } else { None });
|
|
assert!(label.is_some(), "page {i}");
|
|
}
|
|
let c = l.caret_on(&Pos { story: StoryRef::Part(id), path: Path::top(0), off: 0 }, 1).unwrap();
|
|
assert_eq!(c.page, 1);
|
|
assert!(c.top > 700.0);
|
|
}
|
|
|
|
#[test]
|
|
fn tall_first_page_header_pushes_body_down() {
|
|
let mut d = Document::from_text(&"para\n".repeat(120));
|
|
let tall: Vec<_> = (0..12).map(|_| wordcraft_doc::para_block(wordcraft_doc::Paragraph::new())).collect();
|
|
let id = d.add_part(wordcraft_doc::PartKind::Header, tall);
|
|
d.last_section.headers.first = Some(id);
|
|
d.last_section.title_page = true;
|
|
let l = lay(&d);
|
|
let first = l.caret(&Pos::body(0, 0)).unwrap();
|
|
assert!(first.top > d.last_section.margin_top + 50.0, "{first:?}");
|
|
// The first paragraph on page 2 (which has no header) sits higher than page 1's first line,
|
|
// whatever the installed fonts do to pagination.
|
|
let later = (0..120).filter_map(|i| l.caret(&Pos::body(i, 0))).find(|c| c.page == 1).unwrap();
|
|
assert!(later.top < first.top, "{later:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn cache_reuses_unchanged_paragraphs() {
|
|
let d = Document::from_text(&"some text here\n".repeat(50));
|
|
let mut c = LayoutCache::new();
|
|
layout(&d, &mut c, &LayoutOptions::default());
|
|
let misses = c.misses;
|
|
let mut d2 = d.clone();
|
|
d2.insert_text(&Pos::body(10, 0), "x", &Default::default()).unwrap();
|
|
layout(&d2, &mut c, &LayoutOptions::default());
|
|
assert_eq!(c.misses, misses + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_rects_cover_range() {
|
|
let d = Document::from_text("Hello world\nSecond line");
|
|
let l = lay(&d);
|
|
let r = l.selection_rects(&d, &Pos::body(0, 6), &Pos::body(1, 6), 0);
|
|
assert_eq!(r.len(), 2);
|
|
assert!(r[0].1.w > 20.0);
|
|
}
|
|
|
|
#[test]
|
|
fn display_has_glyphs_and_marks() {
|
|
let mut d = Document::from_text("Hello\tworld");
|
|
d.format_range(&Pos::body(0, 0), &Pos::body(0, 5), &|c| c.underline = Some(wordcraft_doc::props::Underline::Single)).unwrap();
|
|
let l = lay(&d);
|
|
let items = display::page_display(&d, &l.pages[0], &display::DisplayOptions { marks: true, ..Default::default() });
|
|
assert!(items.iter().any(|i| matches!(i, display::Draw::Glyphs { .. })));
|
|
assert!(items.iter().any(|i| matches!(i, display::Draw::Line { .. })));
|
|
assert!(items.iter().any(|i| matches!(i, display::Draw::Mark { ch: '¶', .. })));
|
|
assert!(items.iter().any(|i| matches!(i, display::Draw::Mark { ch: '→', .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn web_view_is_one_page() {
|
|
let d = Document::from_text(&"text ".repeat(3000));
|
|
let mut c = LayoutCache::new();
|
|
let l = layout(&d, &mut c, &LayoutOptions { view: ViewMode::Web, web_width: 800.0, show_hidden: false, proofing: false });
|
|
assert_eq!(l.pages.len(), 1);
|
|
assert!(l.pages[0].h > 800.0);
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_props_do_not_panic() {
|
|
let mut d = Document::from_text("x\ny");
|
|
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p| {
|
|
*p = ParaProps { indent_left: Some(1e9), indent_right: Some(1e9), indent_first: Some(-1e9), ..Default::default() }
|
|
})
|
|
.unwrap();
|
|
d.last_section.page_w = 1.0;
|
|
d.last_section.margin_left = 500.0;
|
|
let l = lay(&d);
|
|
assert!(!l.pages.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn layout_is_fast() {
|
|
let d =
|
|
Document::from_text(&"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.\n".repeat(3000));
|
|
let mut c = LayoutCache::new();
|
|
let t0 = now_ms();
|
|
let l = layout(&d, &mut c, &LayoutOptions::default());
|
|
let cold = now_ms() - t0;
|
|
let mut d2 = d.clone();
|
|
d2.insert_text(&Pos::body(1500, 0), "x", &Default::default()).unwrap();
|
|
let t1 = now_ms();
|
|
let _ = layout(&d2, &mut c, &LayoutOptions::default());
|
|
let warm = now_ms() - t1;
|
|
eprintln!("3000 paragraphs, {} pages: cold {cold:.1} ms, warm {warm:.1} ms", l.pages.len());
|
|
assert!(warm < cold);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_center_tab_then_right_tab() {
|
|
let mut d = Document::from_text("left\t\tright");
|
|
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.style = Some("Header".into())).unwrap();
|
|
let l = lay(&d);
|
|
let end = l.caret(&Pos::body(0, 11)).unwrap().x;
|
|
assert!((end - (72.0 + 468.0)).abs() < 1.0, "{end}");
|
|
}
|
|
|
|
#[test]
|
|
fn footnotes_sit_at_page_bottom() {
|
|
let mut d = Document::from_text(&"Body text line.\n".repeat(30));
|
|
let mut note = wordcraft_doc::Paragraph::with_text("The note text.", Default::default()).styled("FootnoteText");
|
|
note.props.space_after = Some(0.0);
|
|
let id = d.add_part(wordcraft_doc::PartKind::Footnote, vec![wordcraft_doc::para_block(note)]);
|
|
d.insert_object(
|
|
&Pos::body(3, 4),
|
|
InlineObject::NoteRef { kind: wordcraft_doc::para::NoteKind::Footnote, id, custom: String::new() },
|
|
&Default::default(),
|
|
)
|
|
.unwrap();
|
|
let eid = d.add_part(
|
|
wordcraft_doc::PartKind::Endnote,
|
|
vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("An endnote.", Default::default()))],
|
|
);
|
|
d.insert_object(
|
|
&Pos::body(5, 2),
|
|
InlineObject::NoteRef { kind: wordcraft_doc::para::NoteKind::Endnote, id: eid, custom: String::new() },
|
|
&Default::default(),
|
|
)
|
|
.unwrap();
|
|
let l = lay(&d);
|
|
let c = l.caret_on(&Pos { story: StoryRef::Part(id), path: Path::top(0), off: 0 }, 0).unwrap();
|
|
assert_eq!(c.page, 0);
|
|
assert!(c.top > 650.0 && c.top < 720.0, "{c:?}");
|
|
let body_bottom = l.pages[0]
|
|
.items
|
|
.iter()
|
|
.filter_map(|it| if let Placed::Lines { story: StoryRef::Body, y, para, l0, l1, .. } = it { item_bottom(*y, para, *l0, *l1) } else { None })
|
|
.fold(0.0f32, f32::max);
|
|
assert!(body_bottom < c.top, "{body_bottom} {}", c.top);
|
|
assert!(l.caret_on(&Pos { story: StoryRef::Part(eid), path: Path::top(0), off: 0 }, 0).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn text_wraps_around_square_float() {
|
|
let mut d = Document::from_text(&"Words flow around the picture here. ".repeat(30));
|
|
let float = wordcraft_doc::para::Float {
|
|
wrap: wordcraft_doc::para::Wrap::Square,
|
|
h_rel: wordcraft_doc::para::Anchor::Column,
|
|
v_rel: wordcraft_doc::para::Anchor::Paragraph,
|
|
x: 0.0,
|
|
y: 0.0,
|
|
dist: 9.0,
|
|
};
|
|
let shape = InlineObject::Shape {
|
|
kind: wordcraft_doc::para::ShapeKind::Rectangle,
|
|
w: 144.0,
|
|
h: 100.0,
|
|
fill: None,
|
|
stroke: None,
|
|
stroke_width: 1.0,
|
|
float,
|
|
story: None,
|
|
};
|
|
d.insert_object(&Pos::body(0, 0), shape, &Default::default()).unwrap();
|
|
let l = lay(&d);
|
|
let Placed::Lines { para, x, .. } = l.pages[0].items.iter().find(|i| matches!(i, Placed::Lines { .. })).unwrap() else { panic!() };
|
|
let first = ¶.lines[0];
|
|
assert!(first.left >= 144.0, "first line starts beside the float: {}", first.left);
|
|
let later = para.lines.iter().find(|ln| ln.top > 120.0).unwrap();
|
|
assert!(later.left < 1.0, "lines below the float use the full width: {}", later.left);
|
|
assert!(x + first.left > 72.0 + 144.0);
|
|
assert!(l.pages[0].items.iter().any(|i| matches!(i, Placed::Shape { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn line_numbers_borders_text_boxes() {
|
|
let mut d = Document::from_text("one\ntwo\nthree");
|
|
d.last_section.line_numbers = Some(Default::default());
|
|
d.last_section.page_borders = Some(wordcraft_doc::props::Borders::box_(wordcraft_doc::props::Border::single(1.0)));
|
|
let id = d.add_part(
|
|
wordcraft_doc::PartKind::TextBox,
|
|
vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("inside the box", Default::default()))],
|
|
);
|
|
let tb = InlineObject::Shape {
|
|
kind: wordcraft_doc::para::ShapeKind::TextBox,
|
|
w: 144.0,
|
|
h: 72.0,
|
|
fill: None,
|
|
stroke: None,
|
|
stroke_width: 1.0,
|
|
float: Default::default(),
|
|
story: Some(id),
|
|
};
|
|
d.insert_object(&Pos::body(2, 5), tb, &Default::default()).unwrap();
|
|
let l = lay(&d);
|
|
let p = &l.pages[0];
|
|
let numbers = p.items.iter().filter(|i| matches!(i, Placed::Lines { story: StoryRef::Part(u32::MAX), .. })).count();
|
|
assert_eq!(numbers, 3);
|
|
assert!(p.items.iter().filter(|i| matches!(i, Placed::Rule { .. })).count() >= 4);
|
|
assert!(l.caret(&Pos { story: StoryRef::Part(id), path: Path::top(0), off: 0 }).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn tall_rows_split_across_pages() {
|
|
let mut d = Document::from_text("before\nafter");
|
|
let mut t = Table::new(2, 2, 468.0);
|
|
let long = "Row text that keeps going and going so the cell grows taller than a page. ".repeat(120);
|
|
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(&long, Default::default()))];
|
|
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t.clone())).unwrap();
|
|
let l = lay(&d);
|
|
assert!(l.pages.len() >= 2, "pages {}", l.pages.len());
|
|
// The cell's lines start on page 1 (row split, not moved) and continue on page 2.
|
|
let cell_lines = |p: &Page| p.items.iter().filter(|i| matches!(i, Placed::Lines { path, .. } if path.0 == vec![1, 0, 0, 0])).count();
|
|
assert!(cell_lines(&l.pages[0]) > 0);
|
|
assert!(cell_lines(&l.pages[1]) > 0);
|
|
// Nothing runs past the bottom margin.
|
|
for p in &l.pages[..l.pages.len() - 1] {
|
|
for it in &p.items {
|
|
if let Placed::Lines { y, para, l0, l1, .. } = it {
|
|
let b = item_bottom(*y, para, *l0, *l1).unwrap_or(0.0);
|
|
assert!(b <= 792.0 - 72.0 + 0.5, "line bottom {b}");
|
|
}
|
|
}
|
|
}
|
|
// Every line of the long paragraph is placed exactly once.
|
|
let total: usize = l
|
|
.pages
|
|
.iter()
|
|
.flat_map(|p| p.items.iter())
|
|
.filter_map(|i| if let Placed::Lines { path, l0, l1, .. } = i { (path.0 == vec![1, 0, 0, 0]).then_some(l1 - l0) } else { None })
|
|
.sum();
|
|
let n = l
|
|
.pages
|
|
.iter()
|
|
.flat_map(|p| p.items.iter())
|
|
.find_map(|i| if let Placed::Lines { path, para, .. } = i { (path.0 == vec![1, 0, 0, 0]).then_some(para.lines.len()) } else { None })
|
|
.unwrap_or(0);
|
|
assert_eq!(total, n);
|
|
// "Can't split" rows move whole instead.
|
|
let mut d2 = Document::from_text("before\nafter");
|
|
t.rows[0].props.cant_split = true;
|
|
d2.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
|
|
let l2 = lay(&d2);
|
|
assert!(l2.pages.len() >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn drop_cap_indents_its_lines() {
|
|
let mut p =
|
|
wordcraft_doc::Paragraph::with_text(&"Every line of this paragraph wraps around a large first letter. ".repeat(6), Default::default());
|
|
p.props.drop_cap = Some(3);
|
|
let mut d = Document::from_text("x");
|
|
d.body = vec![wordcraft_doc::para_block(p)];
|
|
let l = lay(&d);
|
|
let Some(pl) = l.pages[0].items.iter().find_map(|i| if let Placed::Lines { para, .. } = i { Some(para.clone()) } else { None }) else {
|
|
panic!("no lines")
|
|
};
|
|
let (nc, lines, w) = pl.drop_cap.unwrap_or((0, 0, 0.0));
|
|
assert_eq!((nc, lines), (1, 3));
|
|
assert!(w > 20.0, "drop cap width {w}");
|
|
assert!(pl.lines.len() > 4);
|
|
for (k, line) in pl.lines.iter().enumerate() {
|
|
let x = line.xs.first().copied().unwrap_or(0.0);
|
|
if k < 3 {
|
|
assert!(x >= w - 0.5, "line {k} at {x}");
|
|
} else {
|
|
assert!(x < 1.0, "line {k} at {x}");
|
|
}
|
|
}
|
|
// The letter's glyphs are lowered toward the third line's baseline.
|
|
let g = pl.glyphs.first().map(|g| g.dy).unwrap_or(0.0);
|
|
assert!(g < -20.0, "dy {g}");
|
|
}
|
|
|
|
#[test]
|
|
fn auto_hyphenation_breaks_long_words() {
|
|
let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(4);
|
|
let mut d = Document::from_text(&text);
|
|
let off = lay(&d);
|
|
let hyphens = |l: &DocLayout| {
|
|
l.pages[0]
|
|
.items
|
|
.iter()
|
|
.filter_map(|i| if let Placed::Lines { para, .. } = i { Some(para.lines.iter().filter(|x| x.hyphen.is_some()).count()) } else { None })
|
|
.sum::<usize>()
|
|
};
|
|
assert_eq!(hyphens(&off), 0);
|
|
d.settings.auto_hyphenation = true;
|
|
let on = lay(&d);
|
|
assert!(hyphens(&on) > 0, "no hyphenated lines");
|
|
// Hyphenated lines stay inside the margins.
|
|
for it in &on.pages[0].items {
|
|
if let Placed::Lines { para, .. } = it {
|
|
for l in para.lines.iter().filter(|l| l.hyphen.is_some()) {
|
|
assert!(l.xs.last().copied().unwrap_or(0.0) <= l.right + 0.5, "{} > {}", l.xs.last().copied().unwrap_or(0.0), l.right);
|
|
}
|
|
}
|
|
}
|
|
// Soft hyphens break even without auto hyphenation, and show a hyphen.
|
|
let soft = format!("{}extra\u{ad}ordinary", "word ".repeat(13));
|
|
let d2 = Document::from_text(&soft);
|
|
let l2 = lay(&d2);
|
|
let _ = hyphens(&l2);
|
|
}
|