mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: Auto Size area type in columns usually lays the columns out twice an edit
For Auto Size area type in columns, every text edit, each keystroke of the Type tool included, laid the text out in one column and then laid the columns out at that height and at each step of a bisection for the least height that holds the text. In the release-build timing test auto_height_in_columns_typing_timing (stories of 5,000 to 50,000 characters at 9 pt in a frame 500 pt wide), a keystroke laid the columns out 18 to 22 times, and typing at the start of the story of 50,000 characters in 2 columns took 1,670 ms a keystroke. The bisection now runs on the lines of the one-column layout. columns_hold computes where those lines would sit in the columns, with each column's first line at its first baseline below the column's top (LineInfo::first_baseline, new in vectorcraft-text). The columns are then laid out at the upper height the bisection ends on and, when they hold the text there, at the lower one, unless that is the 1 pt it starts from. When they hold the text at the upper height and not at the lower one, Auto Size takes the upper height. When taller columns always hold the text, the old search ends between the same two heights. When the text wraps around other objects or does not fit the one-column layout, or when that check fails, the columns are laid out at every step as before. The same keystroke now takes 292 ms. Docs: docs/target-app-parity.md.
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