mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2065 from dotCooCoo/feat/auto-size-type-fewer-layouts
Type: Auto Size area type in columns usually lays the columns out twice an edit
This commit is contained in:
@@ -124,6 +124,15 @@ fn auto_height_frame(t: &TextObject) -> Option<vectorcraft_geom::Rect> {
|
||||
(finite && b.height() > 1e-6 && b.width() > 1e-6 && (area - b.area()).abs() <= b.area() * 1e-6).then_some(b)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Tests: how many times [`auto_height`] laid out text in its columns on this thread.
|
||||
pub(crate) static COLUMN_LAYOUTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// Tests: when set, [`auto_height`] lays out the columns at every step of its bisection on
|
||||
/// this thread.
|
||||
pub(crate) static FULL_SEARCH: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Auto Size: move the bottom of a rectangular area type frame to just below its last line (plus
|
||||
/// the inset), so the frame's height follows the text. Text in several columns gets the least
|
||||
/// height (to 0.01 pt) at which it fits. True when the frame changed.
|
||||
@@ -158,21 +167,30 @@ fn auto_height(t: &mut TextObject) -> bool {
|
||||
// The least height (to 0.01 pt) at which the columns hold the text.
|
||||
probe.area.columns = cols;
|
||||
let fits = |probe: &mut TextObject, h: f64| {
|
||||
#[cfg(test)]
|
||||
COLUMN_LAYOUTS.with(|c| c.set(c.get() + 1));
|
||||
with_size(probe, b.width(), h);
|
||||
!vectorcraft_text::layout(db, probe).overflow
|
||||
};
|
||||
if !fits(&mut probe, h) {
|
||||
return false;
|
||||
}
|
||||
let (mut lo, mut hi) = (1.0, h);
|
||||
for _ in 0..24 {
|
||||
if hi - lo <= 0.01 {
|
||||
break;
|
||||
// The bisection runs on the one column's lines as `columns_hold` places them in the columns.
|
||||
// The columns are then laid out at the upper height it ends on and, when they hold the text
|
||||
// there, at the lower one, unless that is the 1 pt it starts from. When the one column
|
||||
// overflows, when text wraps around other objects, or when the columns don't hold the text
|
||||
// at the upper height or do hold it at the lower one, the columns are laid out at every step
|
||||
// of the bisection.
|
||||
let quick = !lay.overflow && t.wrap.is_empty();
|
||||
#[cfg(test)]
|
||||
let quick = quick && !FULL_SEARCH.with(std::cell::Cell::get);
|
||||
let ends = quick.then(|| least_height(1.0, h, |x| columns_hold(&lay.lines, cols, b.y0, inset, x)));
|
||||
h = match ends {
|
||||
Some((lo, hi)) if fits(&mut probe, hi) && (lo <= 1.0 || !fits(&mut probe, lo)) => hi,
|
||||
_ => {
|
||||
if !fits(&mut probe, h) {
|
||||
return false;
|
||||
}
|
||||
least_height(1.0, h, |x| fits(&mut probe, x)).1
|
||||
}
|
||||
let mid = (lo + hi) * 0.5;
|
||||
if fits(&mut probe, mid) { hi = mid } else { lo = mid }
|
||||
}
|
||||
h = hi;
|
||||
};
|
||||
}
|
||||
if (h - b.height()).abs() <= 1e-6 {
|
||||
return false;
|
||||
@@ -189,6 +207,38 @@ fn auto_height(t: &mut TextObject) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The bisection [`auto_height`] runs for text in columns, from `lo` (taken not to fit) and `hi`
|
||||
/// (where it fits): at most 24 halvings, until they are at most 0.01 pt apart → (lo, hi).
|
||||
fn least_height(mut lo: f64, mut hi: f64, mut fits: impl FnMut(f64) -> bool) -> (f64, f64) {
|
||||
for _ in 0..24 {
|
||||
if hi - lo <= 0.01 {
|
||||
break;
|
||||
}
|
||||
let mid = (lo + hi) * 0.5;
|
||||
if fits(mid) { hi = mid } else { lo = mid }
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
/// Do `cols` columns of a frame `h` tall (its top at `y0`, the text `inset` inside it) hold
|
||||
/// `lines`, the text's lines laid out in one column as wide as each of them from the same top?
|
||||
/// Each column takes the next lines while they end within its bottom, as layout tests them
|
||||
/// (`baseline + descent <= bottom + 0.01`). A column's first line sits its
|
||||
/// [`first_baseline`](vectorcraft_text::LineInfo::first_baseline) below `y0 + inset`, and the
|
||||
/// lines after it keep their distances from it. A last line with no characters (after a return
|
||||
/// that ends the text) may be left out, as layout leaves it out without overflowing.
|
||||
fn columns_hold(lines: &[vectorcraft_text::LineInfo], cols: usize, y0: f64, inset: f64, h: f64) -> bool {
|
||||
let Some(first) = lines.first() else { return true };
|
||||
let bottom = y0 + h - inset + 0.01;
|
||||
let mut next = 0;
|
||||
for _ in 0..cols {
|
||||
let Some(start) = lines.get(next) else { break };
|
||||
let up = (start.baseline - start.first_baseline) - (first.baseline - first.first_baseline);
|
||||
next += lines.iter().skip(next).take_while(|l| l.baseline + l.descent - up <= bottom).count();
|
||||
}
|
||||
next >= lines.len() || (next + 1 == lines.len() && lines.last().is_some_and(|l| l.start == l.end))
|
||||
}
|
||||
|
||||
/// Recompute the layout bounds cache for the text among `ids` (and their descendants) that lacks
|
||||
/// it. Pasted or imported type arrives without the cache (not serialized), so selection boxes, the
|
||||
/// Transform panel and hit testing would fall back to [`TextObject::estimate_bounds`] (a rough
|
||||
|
||||
@@ -205,6 +205,221 @@ fn auto_height_in_columns_and_by_hand() {
|
||||
assert!(s.execute("text.areaOptions", &json!({"fit": "autoHeight"})).is_err(), "point type has no area options");
|
||||
}
|
||||
|
||||
/// Typing at the start of a story in Auto Size area type with 2 columns lays the columns out at
|
||||
/// most twice a keystroke: the bisection for their height runs on the one column's lines.
|
||||
#[test]
|
||||
fn auto_height_in_columns_lays_out_the_columns_twice_a_keystroke() {
|
||||
use crate::cmd::typecmd::COLUMN_LAYOUTS;
|
||||
let mut s = session();
|
||||
let story = format!("{STORY}\n").repeat(4);
|
||||
let id = area(&mut s, 300.0, 100.0, &story, json!({"fit": "autoHeight"}));
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("text.areaOptions", &json!({"columns": 2, "gutter": 12})).unwrap();
|
||||
let before = frame(&s, id);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("type", v).unwrap();
|
||||
// The caret goes before the first character.
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 41.0, 45.0), v).unwrap();
|
||||
}
|
||||
let typed = "More words at the start, enough for another line or two in the first column. ";
|
||||
for ch in typed.chars() {
|
||||
let start = COLUMN_LAYOUTS.with(std::cell::Cell::get);
|
||||
s.tool_text(&ch.to_string(), v).unwrap();
|
||||
let laid = COLUMN_LAYOUTS.with(std::cell::Cell::get) - start;
|
||||
assert!(laid <= 2, "{ch:?} laid out the columns {laid} times");
|
||||
}
|
||||
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
|
||||
assert_eq!(text(&s, id).plain_text(), format!("{typed}{story}"));
|
||||
assert!(frame(&s, id).height() > before.height(), "the frame follows the text: {before:?} → {:?}", frame(&s, id));
|
||||
assert!(!overflows(&s, id));
|
||||
}
|
||||
|
||||
/// Two sessions run the same keystrokes, edits, restyles, Area Type Options, undos and redos in
|
||||
/// Auto Size area type with 2 and then 3 columns, the second with `FULL_SEARCH` on (the columns
|
||||
/// laid out at every step of the bisection), and the type is the same in both after each step.
|
||||
#[test]
|
||||
fn auto_height_in_columns_matches_the_full_search() {
|
||||
use crate::cmd::typecmd::FULL_SEARCH;
|
||||
enum Step {
|
||||
Click(f64, f64),
|
||||
Type(&'static str),
|
||||
Key(ToolKey),
|
||||
Exec(&'static str, Value),
|
||||
}
|
||||
use Step::*;
|
||||
let v = ViewInfo::default();
|
||||
let story = format!("{STORY}\n{STORY} {STORY}\nShort one.\n{STORY}");
|
||||
let make = || {
|
||||
let mut s = session();
|
||||
let id = area(&mut s, 300.0, 100.0, &story, json!({"fit": "autoHeight"}));
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("text.areaOptions", &json!({"columns": 2, "gutter": 12})).unwrap();
|
||||
(s, id)
|
||||
};
|
||||
let ((mut quick, id), (mut full, full_id)) = (make(), make());
|
||||
assert_eq!(id, full_id);
|
||||
let run = |s: &mut Session, step: &Step| match step {
|
||||
Click(x, y) => {
|
||||
s.select_tool("type", v).unwrap();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, *x, *y), v).unwrap();
|
||||
}
|
||||
}
|
||||
Type(typed) => {
|
||||
for ch in typed.chars() {
|
||||
s.tool_text(&ch.to_string(), v).unwrap();
|
||||
}
|
||||
}
|
||||
Key(k) => {
|
||||
s.tool_key(*k, Mods::default(), v).unwrap();
|
||||
}
|
||||
Exec(cmd, p) => {
|
||||
s.execute(cmd, p).unwrap();
|
||||
}
|
||||
};
|
||||
let mut steps = vec![
|
||||
// Typing at the start, a new paragraph and Backspace, then typing in the second column.
|
||||
Click(41.0, 45.0),
|
||||
Type("Words typed first "),
|
||||
Key(ToolKey::Enter),
|
||||
Type("A new paragraph of a few words"),
|
||||
Key(ToolKey::Backspace),
|
||||
Key(ToolKey::Backspace),
|
||||
Key(ToolKey::Escape),
|
||||
Click(200.0, 50.0),
|
||||
Type(" and more in the second column"),
|
||||
Key(ToolKey::Escape),
|
||||
// A long insertion and a deletion, larger type, paragraph spacing.
|
||||
Exec("text.editRange", json!({"id": id.0, "start": 20, "end": 20, "insert": format!("{STORY} {STORY}")})),
|
||||
Exec("text.editRange", json!({"id": id.0, "start": 0, "end": 60})),
|
||||
Exec("text.setRangeStyle", json!({"id": id.0, "start": 0, "end": 12, "size": 30})),
|
||||
Exec("text.setRangeStyle", json!({"id": id.0, "start": 150, "end": 170, "size": 20})),
|
||||
Exec("select.set", json!({"ids": [id.0]})),
|
||||
Exec("text.setFormat", json!({"spaceBefore": 6, "spaceAfter": 4})),
|
||||
// Three columns, typing in them, Area Type Options and the leading model.
|
||||
Exec("text.areaOptions", json!({"columns": 3})),
|
||||
Click(41.0, 60.0),
|
||||
Type("More"),
|
||||
Key(ToolKey::Escape),
|
||||
Exec("select.set", json!({"ids": [id.0]})),
|
||||
Exec("text.areaOptions", json!({"gutter": 30, "inset": 6})),
|
||||
Exec("text.areaOptions", json!({"firstBaseline": "capHeight"})),
|
||||
Exec("text.areaOptions", json!({"firstBaseline": "fixed", "firstBaselineMin": 20})),
|
||||
Exec("text.areaOptions", json!({"firstBaseline": "leading", "firstBaselineMin": 0})),
|
||||
Exec("text.setFormat", json!({"leadingModel": "emBoxTop"})),
|
||||
];
|
||||
let start = undo_len(&quick);
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
run(&mut quick, step);
|
||||
FULL_SEARCH.with(|c| c.set(true));
|
||||
run(&mut full, step);
|
||||
FULL_SEARCH.with(|c| c.set(false));
|
||||
assert_eq!(text(&quick, id), text(&full, id), "after step {i}");
|
||||
}
|
||||
let edits = undo_len(&quick) - start;
|
||||
assert_eq!(edits, 14, "three typing sessions and eleven edits, one undo step each");
|
||||
steps.clear();
|
||||
steps.extend((0..edits).map(|_| Exec("edit.undo", json!({}))));
|
||||
steps.extend((0..edits).map(|_| Exec("edit.redo", json!({}))));
|
||||
for (i, step) in steps.iter().enumerate() {
|
||||
run(&mut quick, step);
|
||||
FULL_SEARCH.with(|c| c.set(true));
|
||||
run(&mut full, step);
|
||||
FULL_SEARCH.with(|c| c.set(false));
|
||||
assert_eq!(text(&quick, id), text(&full, id), "after undo or redo {i}");
|
||||
}
|
||||
assert_eq!((undo_len(&quick), undo_len(&full)), (start + edits, start + edits));
|
||||
}
|
||||
|
||||
/// A line that starts with a letter smaller than the rest of it can sit higher or lower at the top
|
||||
/// of a column than `columns_hold` places it. When the columns do not hold the text at the upper
|
||||
/// height the search ends on, or do hold it at the lower one, Auto Size lays the columns out at
|
||||
/// every step, and the frame gets the height the full search gives it. In 2 columns, lines in 6 pt
|
||||
/// alternate with lines that start with a 6 pt letter and go on in 30 pt, with auto leading and
|
||||
/// with 36 pt leading, and lines in 6 pt are added at the start one at a time.
|
||||
#[test]
|
||||
fn auto_height_in_columns_matches_the_full_search_when_lines_start_small() {
|
||||
use crate::cmd::typecmd::{COLUMN_LAYOUTS, FULL_SEARCH};
|
||||
let (small, big) = ("small words in six point type\n", "b BIG\n");
|
||||
let story = format!("{small}{big}").repeat(13);
|
||||
for leading in [json!("auto"), json!(36)] {
|
||||
let make = || {
|
||||
let mut s = session();
|
||||
let area = json!({"width": 300, "height": 100, "fit": "autoHeight"});
|
||||
let id = s.execute("text.create", &json!({"x": 40, "y": 40, "size": 6, "area": area, "text": story})).unwrap()["id"].as_u64().unwrap();
|
||||
for k in 0..13 {
|
||||
let start = k * (small.len() + big.len()) + small.len() + 1;
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "start": start, "end": start + 4, "size": 30})).unwrap();
|
||||
}
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "leading": leading})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute("text.areaOptions", &json!({"columns": 2, "gutter": 12})).unwrap();
|
||||
(s, NodeId(id))
|
||||
};
|
||||
let ((mut quick, id), (mut full, _)) = (make(), make());
|
||||
let mut every_step = 0;
|
||||
for i in 0..12 {
|
||||
let add = json!({"id": id.0, "start": 0, "end": 0, "insert": "more small words\n"});
|
||||
let start = COLUMN_LAYOUTS.with(std::cell::Cell::get);
|
||||
quick.execute("text.editRange", &add).unwrap();
|
||||
let laid = COLUMN_LAYOUTS.with(std::cell::Cell::get) - start;
|
||||
every_step += usize::from(laid > 2);
|
||||
FULL_SEARCH.with(|c| c.set(true));
|
||||
full.execute("text.editRange", &add).unwrap();
|
||||
FULL_SEARCH.with(|c| c.set(false));
|
||||
assert_eq!(text(&quick, id), text(&full, id), "leading {leading}, after line {i}");
|
||||
}
|
||||
assert!(every_step > 0, "leading {leading}: the columns were never laid out at every step");
|
||||
}
|
||||
}
|
||||
|
||||
/// Time typing at the start of a story of 5,000, 20,000 and 50,000 characters in Auto Size area
|
||||
/// type in 2 and 3 columns: 20 letters typed one at a time with the Type tool, with Auto Size as it
|
||||
/// runs and with `FULL_SEARCH` on (the columns laid out at every step of the bisection).
|
||||
/// Run with `cargo test --release -p vectorcraft-engine --lib auto_height_in_columns_typing_timing -- --ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore = "manual timing probe; wall-clock performance depends on machine load"]
|
||||
fn auto_height_in_columns_typing_timing() {
|
||||
use crate::cmd::typecmd::{COLUMN_LAYOUTS, FULL_SEARCH};
|
||||
const WORDS: &str = "The quick brown fox jumps over the lazy dog and keeps running far beyond the first frame of this story. ";
|
||||
let v = ViewInfo::default();
|
||||
for n in [5_000, 20_000, 50_000] {
|
||||
let story = format!("{}\n", WORDS.repeat(6).trim_end()).repeat(n / 600 + 1)[..n].to_string();
|
||||
for cols in [2, 3] {
|
||||
let mut row = format!("{n} characters in {cols} columns:");
|
||||
for full in [false, true] {
|
||||
FULL_SEARCH.with(|c| c.set(full));
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 4400, "height": 1200})).unwrap();
|
||||
let area = json!({"width": 500, "height": 100, "fit": "autoHeight"});
|
||||
let id = s.execute("text.create", &json!({"x": 20, "y": 20, "size": 9, "text": story, "area": area})).unwrap()["id"].clone();
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute("text.areaOptions", &json!({"columns": cols, "gutter": 12})).unwrap();
|
||||
s.select_tool("type", v).unwrap();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 21.0, 24.0), v).unwrap();
|
||||
}
|
||||
let layouts = COLUMN_LAYOUTS.with(std::cell::Cell::get);
|
||||
let mut ms: Vec<f64> = "abcdefghijklmnopqrst"
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
let start = std::time::Instant::now();
|
||||
s.tool_text(&ch.to_string(), v).unwrap();
|
||||
start.elapsed().as_secs_f64() * 1000.0
|
||||
})
|
||||
.collect();
|
||||
let per_key = (COLUMN_LAYOUTS.with(std::cell::Cell::get) - layouts) as f64 / ms.len() as f64;
|
||||
ms.sort_by(f64::total_cmp);
|
||||
let how = if full { "every step laid out" } else { "Auto Size" };
|
||||
row += &format!(" {how} {:.1} ms a keystroke ({per_key:.1} column layouts);", ms[ms.len() / 2]);
|
||||
}
|
||||
FULL_SEARCH.with(|c| c.set(false));
|
||||
println!("{row}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrink_text_round_trips_through_the_native_format() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1836,6 +1836,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
||||
x1: x_end,
|
||||
ascent: m.asc,
|
||||
descent: m.desc,
|
||||
first_baseline: if pen.model == LeadingModel::EmBoxTop { m.top.max(pen.fb_min) } else { m.first_baseline(pen.fb, pen.fb_min) },
|
||||
start: if i < n { sg[i].byte } else { pr.start },
|
||||
end: if last_of_para { pr.end } else { sg[end].byte },
|
||||
glyph_start,
|
||||
@@ -1915,6 +1916,7 @@ fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, path: &Path
|
||||
x1: 0.0,
|
||||
ascent: m.0,
|
||||
descent: m.1,
|
||||
first_baseline: 0.0,
|
||||
start: 0,
|
||||
end: text_len,
|
||||
glyph_start: 0,
|
||||
@@ -2000,6 +2002,7 @@ fn on_path(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, path: &Path
|
||||
x1: pe.x,
|
||||
ascent: m.0,
|
||||
descent: m.1,
|
||||
first_baseline: 0.0,
|
||||
start: 0,
|
||||
end: text_len,
|
||||
glyph_start: 0,
|
||||
|
||||
@@ -128,6 +128,14 @@ pub struct LineInfo {
|
||||
pub x1: f64,
|
||||
pub ascent: f64,
|
||||
pub descent: f64,
|
||||
/// Area type: how far the line's baseline sits below the top of its frame cell (inside the
|
||||
/// inset, with the lines aligned to the top) when the line starts the cell: the metric of the
|
||||
/// line that Area Type Options › First Baseline picks (the em box top with top-to-top leading),
|
||||
/// at least the First Baseline minimum. When layout moves a line on from the cell before and
|
||||
/// the line's first character has smaller metrics than the rest of it, or moves the line down
|
||||
/// to where it has room (in a frame that is not a rectangle, or beside wrap objects), the line
|
||||
/// can sit elsewhere. It is 0 for type on a path.
|
||||
pub first_baseline: f64,
|
||||
/// Byte range of the line in the plain text (excludes the paragraph's `\n`).
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
|
||||
@@ -613,6 +613,46 @@ fn inset_and_first_baseline_options() {
|
||||
assert!(cap.lines[0].baseline < base.lines[0].baseline && cap.lines[0].baseline > 5.0);
|
||||
}
|
||||
|
||||
/// `LineInfo::first_baseline`: each line that starts a column has its baseline that far below the
|
||||
/// column's top inside the inset, for every first baseline option, with and without a minimum and
|
||||
/// an inset, in both leading models, and for lines of several sizes (each line in one size).
|
||||
#[test]
|
||||
fn lines_starting_a_column_sit_at_their_first_baseline() {
|
||||
let paras = [
|
||||
("A heading in large type\n", 24.0),
|
||||
("Body copy in a smaller size that runs over two or three lines of a column.\n", 12.0),
|
||||
("A middle size\n", 18.0),
|
||||
];
|
||||
let runs: Vec<TextRun> = paras.repeat(4).into_iter().map(|(text, size)| run(text, size)).collect();
|
||||
let text: String = runs.iter().map(|r| r.text.as_str()).collect();
|
||||
let mut other_sizes = 0;
|
||||
for model in [vectorcraft_doc::LeadingModel::RomanBaseline, vectorcraft_doc::LeadingModel::EmBoxTop] {
|
||||
for first_baseline in [FirstBaseline::Ascent, FirstBaseline::CapHeight, FirstBaseline::XHeight, FirstBaseline::Leading, FirstBaseline::Fixed]
|
||||
{
|
||||
for (first_baseline_min, inset) in [(0.0, 0.0), (18.0, 6.0)] {
|
||||
for height in [100.0, 120.0, 140.0] {
|
||||
let mut t = area(&text, style(12.0), Rect::new(0.0, 0.0, 420.0, height), Justify::Left);
|
||||
t.runs = runs.clone();
|
||||
t.para.leading_model = model;
|
||||
let opts = LayoutOptions { columns: 3, gutter: 10.0, first_baseline, first_baseline_min, inset, ..Default::default() };
|
||||
let l = layout_with(db(), &t, &opts);
|
||||
let starts: Vec<&LineInfo> =
|
||||
l.lines.iter().enumerate().filter(|(i, li)| *i == 0 || l.lines[i - 1].region != li.region).map(|(_, li)| li).collect();
|
||||
assert_eq!(starts.len(), 3, "{model:?} {first_baseline:?} {height}");
|
||||
other_sizes += starts.iter().filter(|li| li.ascent != starts[0].ascent).count();
|
||||
for li in starts {
|
||||
assert!(
|
||||
(li.baseline - (inset + li.first_baseline)).abs() < 1e-9,
|
||||
"{model:?} {first_baseline:?} {first_baseline_min} {height}: {li:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(other_sizes >= 20, "lines of another size than the first start {other_sizes} columns");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_and_columns_flow_in_order() {
|
||||
let text = COPY.repeat(2);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user