mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-31 22:07:06 -06:00
221 lines
9.3 KiB
Rust
221 lines
9.3 KiB
Rust
//! #3977: floating tables include a line's bottom edge; shapes keep a strict boundary.
|
|
//! The expected distinction is cyril265's Word for Mac 16.114.1 observation in the issue.
|
|
|
|
use super::*;
|
|
use wordcraft_doc::para::{Anchor, Float, InlineObject, ShapeKind, Wrap};
|
|
use wordcraft_doc::props::{HeightRule, LineSpacing, ParaProps, TableFloat};
|
|
use wordcraft_doc::{CharProps, Paragraph, Pos, Table};
|
|
|
|
const LINE: f32 = 82.875;
|
|
const CONTACT: f32 = 403.5;
|
|
|
|
fn document(mode: u32) -> Document {
|
|
let mut d = Document::new();
|
|
d.settings.compat_mode = mode;
|
|
let ch = CharProps { font: Some("Source Sans 3".into()), size: Some(62.5), ..Default::default() };
|
|
d.body = (1..=6)
|
|
.map(|n| {
|
|
let mut p = Paragraph::with_text(&format!("P{n}"), ch.clone());
|
|
p.mark = ch.clone();
|
|
p.props = ParaProps {
|
|
space_before: Some(0.0),
|
|
space_after: Some(0.0),
|
|
line_spacing: Some(LineSpacing::Multiple(1.0)),
|
|
widow_control: Some(false),
|
|
..Default::default()
|
|
};
|
|
wordcraft_doc::para_block(p)
|
|
})
|
|
.collect();
|
|
d
|
|
}
|
|
|
|
fn add_table(d: &mut Document, top: f32, width: f32) {
|
|
let mut t = Table::new(1, 1, width);
|
|
t.props.style = None;
|
|
t.props.width = Some(width);
|
|
t.props.fixed = true;
|
|
t.props.cell_margins = Some([0.0; 4]);
|
|
t.props.float = Some(TableFloat { h_rel: Anchor::Page, v_rel: Anchor::Page, x: 72.0, y: top, ..Default::default() });
|
|
t.rows[0].props.height = Some(50.0);
|
|
t.rows[0].props.height_rule = HeightRule::Exact;
|
|
d.body.insert(0, Arc::new(Block::Table(t)));
|
|
}
|
|
|
|
fn add_shape(d: &mut Document, top: f32, left: f32, width: f32, height: f32) {
|
|
let shape = InlineObject::Shape {
|
|
kind: ShapeKind::Rectangle,
|
|
w: width,
|
|
h: height,
|
|
fill: None,
|
|
stroke: None,
|
|
stroke_width: 0.0,
|
|
float: Float { wrap: Wrap::Square, h_rel: Anchor::Page, v_rel: Anchor::Page, x: left, y: top, ..Default::default() },
|
|
story: None,
|
|
effects: Default::default(),
|
|
freeform: None,
|
|
extra: Default::default(),
|
|
};
|
|
let block = d.body.iter().position(|b| matches!(&**b, Block::Para(_))).unwrap();
|
|
d.insert_object(&Pos::body(block, 0), shape, &Default::default()).unwrap();
|
|
}
|
|
|
|
fn line_positions(d: &Document) -> Vec<(f32, f32, f32)> {
|
|
let l = layout(d, &mut LayoutCache::new(), &LayoutOptions::default());
|
|
assert_eq!(l.pages.len(), 1);
|
|
let lines: Vec<_> = l.pages[0]
|
|
.items
|
|
.iter()
|
|
.filter_map(|item| match item {
|
|
Placed::Lines { path, x, y, para, story: StoryRef::Body, .. } if path.0.len() == 1 => {
|
|
assert_eq!(para.lines.len(), 1);
|
|
let line = ¶.lines[0];
|
|
// Placed::Lines::y is already the top of its first displayed line.
|
|
Some((x + line.left, *y, line.height))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(lines.len(), 6);
|
|
lines
|
|
}
|
|
|
|
#[test]
|
|
fn only_tables_include_exact_line_bottom_contact_in_all_compatibility_modes() {
|
|
for mode in [11, 12, 14, 15] {
|
|
let control = line_positions(&document(mode));
|
|
assert_eq!(control[3].1 + control[3].2, CONTACT);
|
|
assert!(control.iter().enumerate().all(|(n, l)| l.1 == 72.0 + n as f32 * LINE && l.2 == LINE));
|
|
for table in [false, true] {
|
|
for twips in [-1, 0, 1] {
|
|
let top = CONTACT + twips as f32 / 20.0;
|
|
let mut d = document(mode);
|
|
if table {
|
|
add_table(&mut d, top, 468.0);
|
|
} else {
|
|
add_shape(&mut d, top, 72.0, 468.0, 50.0);
|
|
}
|
|
let lines = line_positions(&d);
|
|
let moves = twips < 0 || (table && twips == 0);
|
|
let expected = if moves { top + 50.0 } else { control[3].1 };
|
|
assert!((lines[3].1 - expected).abs() < 0.001, "mode={mode}, table={table}, twips={twips}: {lines:?}");
|
|
assert_eq!(&lines[..3], &control[..3]);
|
|
// If P4 stays above, P5 is the first line that meets the object.
|
|
let next = if moves { lines[3].1 + LINE } else { top + 50.0 };
|
|
assert!((lines[4].1 - next).abs() < 0.001, "mode={mode}, table={table}, twips={twips}: {lines:?}");
|
|
assert_eq!(lines[5].1, lines[4].1 + LINE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_word_skipping_a_narrow_strip_at_table_contact_resumes_when_the_table_ends() {
|
|
for mode in [11, 12, 14, 15] {
|
|
let mut d = document(mode);
|
|
// Before contact the shape leaves 200 pt on the left. At contact the table takes
|
|
// 168 pt of that, leaving a 32 pt strip, too narrow for P4. The table ends before
|
|
// the shape: P4 must use the newly available left strip, not jump below the shape.
|
|
add_table(&mut d, CONTACT, 168.0);
|
|
add_shape(&mut d, 72.0, 272.0, 268.0, 500.0);
|
|
let lines = line_positions(&d);
|
|
assert!((lines[3].1 - (CONTACT + 50.0)).abs() < 0.001, "mode={mode}: {lines:?}");
|
|
assert_eq!(lines[3].0, 72.0);
|
|
assert!(lines[..3].iter().enumerate().all(|(n, l)| l.1 == 72.0 + n as f32 * LINE));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_spacing_does_not_make_mark_height_a_table_contact_boundary() {
|
|
for mode in [11, 12, 14, 15] {
|
|
for table in [false, true] {
|
|
let mut d = Document::new();
|
|
d.settings.compat_mode = mode;
|
|
let mut p = Paragraph::with_text("P1", CharProps { font: Some("Source Sans 3".into()), size: Some(11.0), ..Default::default() });
|
|
p.mark = CharProps { font: Some("Source Sans 3".into()), size: Some(62.5), ..Default::default() };
|
|
p.props =
|
|
ParaProps { line_spacing: Some(LineSpacing::Exactly(12.0)), space_before: Some(0.0), space_after: Some(0.0), ..Default::default() };
|
|
d.body = vec![wordcraft_doc::para_block(p)];
|
|
// The mark's natural 82.875 pt estimate ends far below the actual 12 pt line.
|
|
let top = 72.0 + LINE;
|
|
if table {
|
|
add_table(&mut d, top, 468.0);
|
|
} else {
|
|
add_shape(&mut d, top, 72.0, 468.0, 50.0);
|
|
}
|
|
let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default());
|
|
let (y, height) = l.pages[0]
|
|
.items
|
|
.iter()
|
|
.find_map(|item| match item {
|
|
Placed::Lines { story: StoryRef::Body, path, y, para, .. } if path.0.len() == 1 => Some((*y, para.lines[0].height)),
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(height, 12.0);
|
|
assert_eq!(y, 72.0, "mode={mode}, table={table}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn exact_document(mode: u32, mark_size: f32) -> Document {
|
|
let mut d = Document::new();
|
|
d.settings.compat_mode = mode;
|
|
let mut p = Paragraph::with_text("WWWWWWWWWWWW", CharProps { font: Some("Source Sans 3".into()), size: Some(11.0), ..Default::default() });
|
|
p.mark = CharProps { font: Some("Source Sans 3".into()), size: Some(mark_size), ..Default::default() };
|
|
p.props = ParaProps { line_spacing: Some(LineSpacing::Exactly(12.0)), space_before: Some(0.0), space_after: Some(0.0), ..Default::default() };
|
|
d.body = vec![wordcraft_doc::para_block(p)];
|
|
d
|
|
}
|
|
|
|
fn exact_line(d: &Document) -> (f32, f32) {
|
|
let l = layout(d, &mut LayoutCache::new(), &LayoutOptions::default());
|
|
assert_eq!(l.pages.len(), 1);
|
|
l.pages[0]
|
|
.items
|
|
.iter()
|
|
.find_map(|item| match item {
|
|
Placed::Lines { story: StoryRef::Body, path, x, y, para, .. } if path.0 == vec![(d.body.len() - 1) as u32] => {
|
|
assert_eq!(para.lines.len(), 1);
|
|
assert_eq!(para.lines[0].height, 12.0);
|
|
Some((x + para.lines[0].left, *y))
|
|
}
|
|
_ => None,
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn exact_table_contact_uses_the_spacing_with_larger_and_smaller_marks() {
|
|
for mode in [11, 12, 14, 15] {
|
|
for mark in [4.0, 62.5] {
|
|
for twips in [-1, 0, 1] {
|
|
let mut d = exact_document(mode, mark);
|
|
let top = 84.0 + twips as f32 / 20.0;
|
|
add_table(&mut d, top, 468.0);
|
|
let (_, y) = exact_line(&d);
|
|
let expected = if twips <= 0 { top + 50.0 } else { 72.0 };
|
|
assert!((y - expected).abs() < 0.001, "mode={mode}, mark={mark}, twips={twips}: {y}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_table_contact_uses_the_spacing_when_a_word_skips_a_strip() {
|
|
for mode in [11, 12, 14, 15] {
|
|
let mut d = exact_document(mode, 4.0);
|
|
// Anchor the shape in an earlier paragraph so the tested word has no inline anchor.
|
|
let Block::Para(p) = &*d.body[0] else { panic!("expected paragraph") };
|
|
let mut anchor = Paragraph::with_text("P0", p.mark.clone());
|
|
anchor.mark = p.mark.clone();
|
|
anchor.props = p.props.clone();
|
|
d.body.insert(0, wordcraft_doc::para_block(anchor));
|
|
add_table(&mut d, 96.0, 168.0);
|
|
add_shape(&mut d, 72.0, 272.0, 268.0, 500.0);
|
|
// The 32 pt strip cannot hold the word. The table ends before the shape,
|
|
// leaving 200 pt on the left, so skipping must stop at the table's bottom.
|
|
assert_eq!(exact_line(&d), (72.0, 146.0), "mode={mode}");
|
|
}
|
|
}
|