mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: the Every-line Composer composes paragraphs that mix type sizes in rectangular area type
A paragraph with one character in a different type size was broken with the Single-line Composer, even when Every-line was chosen: the composer gave up whenever the paragraph's glyphs did not all have the same line metrics (ascent, descent, leading, cap height, x height and em box top). In rectangular area type without text wrap, rows and columns included, the width of a line does not depend on how far down the frame it is. There the Every-line Composer now composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text), and each line takes its height from its own text, as with the Single-line Composer. Area type of another shape, or with text wrap, still breaks such a paragraph with the Single-line Composer. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -1643,8 +1643,9 @@ fn candidates(text: &str, g: &[SGlyph], hyphenate: Option<&HyphenationOptions>,
|
||||
/// Paragraphs longer than this (glyphs) are always broken line by line.
|
||||
const MAX_COMPOSE_GLYPHS: usize = 200_000;
|
||||
|
||||
/// Every-line composition of paragraph glyphs `sg` if applicable (area text with uniform line
|
||||
/// metrics, justified or ragged); `None` falls back to the greedy single-line composer.
|
||||
/// Every-line composition of paragraph glyphs `sg` if applicable (area text, justified or ragged,
|
||||
/// with uniform line metrics or in rectangular cells without text wrap); `None` falls back to the
|
||||
/// greedy single-line composer.
|
||||
fn compose_para(cx: &Ctx<'_>, sg: &[SGlyph], flex: &[Flex], para: &ParaStyle, pen: &Pen<'_>, rtl: bool) -> Option<Vec<(usize, bool)>> {
|
||||
let ragged = matches!(para.justify, Justify::Auto | Justify::Left | Justify::Center | Justify::Right);
|
||||
let composer = cx.opts.composer.unwrap_or(para.composer);
|
||||
@@ -1658,10 +1659,16 @@ fn compose_para(cx: &Ctx<'_>, sg: &[SGlyph], flex: &[Flex], para: &ParaStyle, pe
|
||||
return None;
|
||||
}
|
||||
let m = Metrics::of(&sg[0]);
|
||||
if sg.iter().any(|g| Metrics::of(g) != m) {
|
||||
// Line widths are independent of the breaks when every line has the same metrics, or when
|
||||
// a line's width does not depend on how far down the frame it is set: rectangular cells
|
||||
// without text wrap. Then a paragraph whose characters differ in line metrics (type size,
|
||||
// font, leading, vertical scale, an inline graphic taller than the text) is composed too. In
|
||||
// other shapes, or with text wrap, a taller line can move the lines below it to where the
|
||||
// frame is narrower, so such a paragraph is broken line by line.
|
||||
let plain = pen.regions.is_some_and(|r| r.iter().all(|r| (r.rect || r.polys.is_empty()) && r.wraps.is_empty()));
|
||||
if !plain && sg.iter().any(|g| Metrics::of(g) != m) {
|
||||
return None;
|
||||
}
|
||||
// Line widths are independent of the breaks when every line has the same metrics.
|
||||
let total: f64 = sg.iter().map(|g| g.adv).sum();
|
||||
let mut sim = pen.clone();
|
||||
let mut widths = vec![];
|
||||
|
||||
@@ -294,6 +294,99 @@ fn composer_x_inline_graphics() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_x_mixed_sizes() {
|
||||
// A 24 pt word among 12 pt text: in a rectangle the every-line composer breaks the paragraph as
|
||||
// it breaks uniform 12 pt text in which that word is set at 200% horizontal scale, as wide as
|
||||
// at 24 pt, and not as the single-line composer does.
|
||||
let st = sans(12.0);
|
||||
let runs = |x: CharStyle| vec![run("Destroy all creatures with toughness ", st.clone()), run("X", x), run(" or less.", st.clone())];
|
||||
let big = sans(24.0);
|
||||
let wide = CharStyle { h_scale: 200.0, ..sans(12.0) };
|
||||
let mut probe = area(runs(big.clone()), Rect::new(0.0, 0.0, 10_000.0, 100.0));
|
||||
probe.runs[2] = run(" or", st.clone());
|
||||
let w = layout(db(), &probe).lines[0].x1 + 2.0;
|
||||
let breaks = |x: &CharStyle, composer: Composer| {
|
||||
let mut t = area(runs(x.clone()), Rect::new(0.0, 0.0, w, 200.0));
|
||||
t.para.composer = composer;
|
||||
line_texts(&t, &layout(db(), &t))
|
||||
};
|
||||
let greedy = ["Destroy all creatures with toughness X or", "less."];
|
||||
let every = ["Destroy all creatures with toughness X", "or less."];
|
||||
assert_eq!(breaks(&wide, Composer::EveryLine), every, "uniform sizes");
|
||||
assert_eq!(breaks(&wide, Composer::SingleLine), greedy, "uniform sizes");
|
||||
assert_eq!(breaks(&big, Composer::EveryLine), every, "mixed sizes");
|
||||
assert_eq!(breaks(&big, Composer::SingleLine), greedy, "mixed sizes");
|
||||
// The same in a rectangle split into two columns, one line in each.
|
||||
let mut t = area(runs(big.clone()), Rect::new(0.0, 0.0, 2.0 * w + 12.0, 34.0));
|
||||
(t.area.columns, t.area.gutter) = (2, 12.0);
|
||||
t.para.composer = Composer::EveryLine;
|
||||
let l = layout(db(), &t);
|
||||
assert_eq!(line_texts(&t, &l), every, "two columns");
|
||||
assert!(!l.overflow && l.lines[1].x0 > w, "{:?}", l.lines);
|
||||
// The 24 pt word's line is set at its own size.
|
||||
let mut t = area(runs(big.clone()), Rect::new(0.0, 0.0, w, 200.0));
|
||||
t.para.composer = Composer::EveryLine;
|
||||
let l = layout(db(), &t);
|
||||
let x = l.glyphs.iter().find(|g| t.plain_text()[g.byte..].starts_with('X')).unwrap();
|
||||
assert_eq!(x.line, 0);
|
||||
assert!(l.lines[0].ascent > l.lines[1].ascent + 1.0, "{:?}", l.lines);
|
||||
// A rectangle that wraps around an object, and a frame that isn't a rectangle (a corner cut),
|
||||
// both far below the text, break mixed sizes line by line and still break uniform sizes with
|
||||
// the every-line composer.
|
||||
let below = PathData::from_bezpath(&Rect::new(0.0, 180.0, 20.0, 190.0).to_path(0.1));
|
||||
for (x, want) in [(&wide, every), (&big, greedy)] {
|
||||
let mut t = area(runs(x.clone()), Rect::new(0.0, 0.0, w, 200.0));
|
||||
t.wrap = vec![vectorcraft_doc::WrapShape { path: below.clone(), wrap: Default::default() }];
|
||||
t.para.composer = Composer::EveryLine;
|
||||
assert_eq!(line_texts(&t, &layout(db(), &t)), want, "wrap, {}", x.size);
|
||||
}
|
||||
let cut = kurbo::BezPath::from_vec(vec![
|
||||
kurbo::PathEl::MoveTo((0.0, 0.0).into()),
|
||||
kurbo::PathEl::LineTo((w, 0.0).into()),
|
||||
kurbo::PathEl::LineTo((w, 190.0).into()),
|
||||
kurbo::PathEl::LineTo((w - 10.0, 200.0).into()),
|
||||
kurbo::PathEl::LineTo((0.0, 200.0).into()),
|
||||
kurbo::PathEl::ClosePath,
|
||||
]);
|
||||
for (x, want) in [(&wide, every), (&big, greedy)] {
|
||||
let mut t = area(runs(x.clone()), Rect::ZERO);
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&cut) };
|
||||
t.para.composer = Composer::EveryLine;
|
||||
assert_eq!(line_texts(&t, &layout(db(), &t)), want, "cut corner, {}", x.size);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_x_mixed_line_metrics() {
|
||||
// In a rectangle the every-line composer also breaks a paragraph whose characters differ in
|
||||
// other line metrics: vertical scale, leading, font, and an inline graphic raised above the text.
|
||||
let st = sans(12.0);
|
||||
let wide = CharStyle { h_scale: 200.0, ..sans(12.0) };
|
||||
let xs = [
|
||||
("vertical scale", run("X", CharStyle { v_scale: 200.0, ..wide.clone() })),
|
||||
("leading", run("X", CharStyle { leading: Some(30.0), ..wide.clone() })),
|
||||
("font", run("X", serif(12.0, 0.0))),
|
||||
("inline graphic", sym(0.7, 20.0, st.clone())),
|
||||
];
|
||||
let mut wrong = vec![];
|
||||
for (name, x) in xs {
|
||||
let runs = |end: &str| vec![run("Destroy all creatures with toughness ", st.clone()), x.clone(), run(end, st.clone())];
|
||||
let w = layout(db(), &area(runs(" or"), Rect::new(0.0, 0.0, 10_000.0, 100.0))).lines[0].x1 + 2.0;
|
||||
let greedy = [format!("Destroy all creatures with toughness {} or", x.text), "less.".to_string()];
|
||||
let every = [format!("Destroy all creatures with toughness {}", x.text), "or less.".to_string()];
|
||||
for (composer, want) in [(Composer::EveryLine, every), (Composer::SingleLine, greedy)] {
|
||||
let mut t = area(runs(" or less."), Rect::new(0.0, 0.0, w, 200.0));
|
||||
t.para.composer = composer;
|
||||
let got = line_texts(&t, &layout(db(), &t));
|
||||
if got != want {
|
||||
wrong.push((name, composer, got));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{wrong:?}");
|
||||
}
|
||||
|
||||
const INLINE: char = vectorcraft_doc::text::INLINE_CHAR;
|
||||
|
||||
// ---------- vertical alignment × … ----------
|
||||
|
||||
+3
-1
@@ -4441,7 +4441,9 @@ when it is Single-line). Every-line, the default, picks the breaks of the whole
|
||||
justified text gets even word spacing, ragged text (left, centre or right aligned) an even rag, so it may move a word
|
||||
that fits to the next line to avoid a lone short word on the last line or a full line next to a short one.
|
||||
Single-line fills each line as far as it goes. Point type and type on a path have no line width, so both composers
|
||||
give the same result there; paragraphs that mix type sizes fall back to Single-line.
|
||||
give the same result there. A paragraph whose characters differ in line metrics (type size, font, leading or vertical
|
||||
scale, or an inline graphic taller than the text) is composed with Every-line in rectangular area type, rows and
|
||||
columns included; in area type of another shape, or with text wrap, it falls back to Single-line.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"ids":[7],"composer":"singleLine"}}}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user