Merge pull request #1617 from marcoarnulfo/fix/area-type-narrow-top-lines
Area type: a word too wide for the top lines of a shape moves down instead of breaking
This commit is contained in:
commit
f870f3ac09
|
|
@ -1013,6 +1013,8 @@ struct Pen<'r> {
|
|||
/// Leading measured from em box top to em box top: where the next line's em box top goes.
|
||||
model: LeadingModel,
|
||||
next_top: Option<f64>,
|
||||
/// The narrowest word no line from here down is wide enough for (see [`Pen::place`]).
|
||||
no_room_for: f64,
|
||||
}
|
||||
|
||||
/// The largest number of steps layout takes to move a line down a frame cell to find room for it:
|
||||
|
|
@ -1020,10 +1022,27 @@ struct Pen<'r> {
|
|||
const MAX_STEPS: usize = 100_000;
|
||||
|
||||
impl Pen<'_> {
|
||||
/// Baseline and horizontal span for a line with estimated metrics `est` and indents; `None` =
|
||||
/// the frame is full (overflow).
|
||||
/// Baseline and horizontal span for a line with estimated metrics `est` and indents that starts
|
||||
/// with a word `word` wide; `None` = the frame is full (overflow).
|
||||
/// In a frame that isn't a rectangle, lines too short for the word are left empty and it moves
|
||||
/// down to the first one wide enough. Only when no line is does it go on the first line with
|
||||
/// room for any text, and get broken there.
|
||||
/// The `bool` is true for a further span at the previous line's baseline.
|
||||
fn place(&mut self, est: Metrics, ind_l: f64, ind_r: f64) -> Option<(f64, f64, f64, bool)> {
|
||||
fn place(&mut self, est: Metrics, ind_l: f64, ind_r: f64, word: f64) -> Option<(f64, f64, f64, bool)> {
|
||||
if word > 0.0 && word < self.no_room_for {
|
||||
let mut pen = self.clone();
|
||||
if let Some(placed) = pen.place_at_least(est, ind_l, ind_r, word) {
|
||||
*self = pen;
|
||||
return Some(placed);
|
||||
}
|
||||
// Lines further down are no wider for this word: don't search for it, or a wider one, again.
|
||||
self.no_room_for = word;
|
||||
}
|
||||
self.place_at_least(est, ind_l, ind_r, 0.0)
|
||||
}
|
||||
|
||||
/// [`Pen::place`], skipping lines of a non-rectangular frame narrower than `min`.
|
||||
fn place_at_least(&mut self, est: Metrics, ind_l: f64, ind_r: f64, min: f64) -> Option<(f64, f64, f64, bool)> {
|
||||
let Some(regions) = self.regions else {
|
||||
let b = self.prev.map_or(0.0, |b| self.next_baseline(b, est));
|
||||
return Some((b, f64::NEG_INFINITY, f64::INFINITY, false));
|
||||
|
|
@ -1046,7 +1065,12 @@ impl Pen<'_> {
|
|||
if baseline + est.desc > r.bottom() + 0.01 {
|
||||
break;
|
||||
}
|
||||
let fits = |&(a, b): &(f64, f64)| b - a - ind_l - ind_r > est.asc.max(1.0);
|
||||
// A rectangle's lines are all as wide: none below has more room for the word.
|
||||
let min = if r.rect || r.polys.is_empty() { 0.0 } else { min };
|
||||
let fits = |&(a, b): &(f64, f64)| {
|
||||
let room = b - a - ind_l - ind_r;
|
||||
room > est.asc.max(1.0) && room + EPS >= min
|
||||
};
|
||||
if r.wraps.is_empty() {
|
||||
match r.span(baseline - est.asc, baseline + est.desc) {
|
||||
Some(s) if fits(&s) => return Some((baseline, s.0, s.1, false)),
|
||||
|
|
@ -1114,6 +1138,25 @@ fn tab_advance(tabs: &[vectorcraft_doc::TabStop], origin: f64, x: f64, rest: &[S
|
|||
(at - rel).max(0.0)
|
||||
}
|
||||
|
||||
/// Width of the word starting at glyph `i`: its advances up to the first break opportunity (spaces
|
||||
/// left out, the hyphen of a soft hyphen in), the least room a line needs not to break inside it.
|
||||
fn word_width(g: &[SGlyph], i: usize) -> f64 {
|
||||
let mut w = 0.0;
|
||||
for gl in g.get(i..).unwrap_or_default() {
|
||||
if gl.is_space() {
|
||||
break;
|
||||
}
|
||||
if gl.is_soft_hyphen() {
|
||||
return w + hyphen_glyph(gl).adv;
|
||||
}
|
||||
w += gl.adv;
|
||||
if gl.break_after() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// Greedy break: returns (end glyph index, hyphenated) for a line starting at `i` of `width`.
|
||||
/// With burasagari, a comma or full stop that doesn't fit ends the line, hanging outside it.
|
||||
fn break_line(text: &str, g: &[SGlyph], i: usize, width: f64, hyphenate: bool, burasagari: Burasagari, kinsoku: Kinsoku) -> (usize, bool) {
|
||||
|
|
@ -1450,7 +1493,10 @@ fn compose_para(cx: &Ctx<'_>, sg: &[SGlyph], para: &ParaStyle, pen: &Pen<'_>, rt
|
|||
while widths.len() < sg.len() {
|
||||
let first = widths.is_empty();
|
||||
let ind_l = para.left_indent + if first { para.first_line_indent } else { 0.0 };
|
||||
let Some((b, x0, x1, _)) = sim.place(m, ind_l, para.right_indent) else { break };
|
||||
// Only the first line's first word is known here. Later lines are placed as the composed
|
||||
// lines need: each fits its width, so its first word does too.
|
||||
let word = if first { word_width(sg, 0) } else { 0.0 };
|
||||
let Some((b, x0, x1, _)) = sim.place(m, ind_l, para.right_indent, word) else { break };
|
||||
let w = x1 - x0 - ind_l - para.right_indent;
|
||||
widths.push(w);
|
||||
acc += w.max(1.0);
|
||||
|
|
@ -1492,6 +1538,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
|||
queued: vec![],
|
||||
model: t.para_at(0).leading_model,
|
||||
next_top: None,
|
||||
no_room_for: f64::INFINITY,
|
||||
};
|
||||
'paras: for (pi, pr) in paras.iter().enumerate() {
|
||||
let para = t.para_at(pi);
|
||||
|
|
@ -1527,6 +1574,8 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
|||
let est = if i < n { Metrics::of(&sg[i]) } else { pm };
|
||||
let first_line = li_para == 0;
|
||||
let ind_l = para.left_indent + if first_line { para.first_line_indent } else { 0.0 };
|
||||
// Measured before Mojikumi narrows an opening bracket, as the composer measures it.
|
||||
let word = word_width(&sg, i);
|
||||
// Mojikumi: an opening bracket starting a line is set flush with the line's start (the
|
||||
// space before it goes), and the line has that much more room. At a paragraph's start
|
||||
// that is the first-line indent: JIS X 4051's principle (JLREQ 3.1.5, Figure 71 ①).
|
||||
|
|
@ -1543,7 +1592,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: Optio
|
|||
// next row/column if it no longer fits).
|
||||
let (baseline, x0, x1, end, hyph, m) = loop {
|
||||
let first_in_region = pen.prev.is_none();
|
||||
let Some((mut baseline, x0, x1, same_baseline)) = pen.place(est, ind_l, para.right_indent) else {
|
||||
let Some((mut baseline, x0, x1, same_baseline)) = pen.place(est, ind_l, para.right_indent, word) else {
|
||||
cx.out.overflow = cx.text.len() > if i < n { sg[i].byte } else { pr.start };
|
||||
break 'paras;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -212,6 +212,38 @@ fn area_flow_inside_circle_keeps_glyphs_inside() {
|
|||
assert!(w(l.lines.len() / 2) > w(0));
|
||||
}
|
||||
|
||||
/// In a frame that is narrow at the top, a word that doesn't fit the top lines moves down to the
|
||||
/// first line wide enough for it (the lines above stay empty), with either composer. Only a word
|
||||
/// wider than every line is broken.
|
||||
#[test]
|
||||
fn area_flow_moves_a_word_down_to_a_line_wide_enough_for_it() {
|
||||
let triangle = |base: f64, height: f64| {
|
||||
let mut p = BezPath::new();
|
||||
p.move_to((base / 2.0, 0.0));
|
||||
p.line_to((base, height));
|
||||
p.line_to((0.0, height));
|
||||
p.close_path();
|
||||
p
|
||||
};
|
||||
let text = "Wonderful extraordinary typesetting";
|
||||
for composer in [Composer::EveryLine, Composer::SingleLine] {
|
||||
let mut t = area_path(text, style(12.0), &triangle(200.0, 200.0), Justify::Left);
|
||||
t.para.composer = composer;
|
||||
let l = layout(db(), &t);
|
||||
assert!(!l.overflow, "{composer:?}");
|
||||
let lines: Vec<&str> = l.lines.iter().map(|li| text[li.start..li.end].trim_end()).collect();
|
||||
// The top lines are wide enough for "Wo" but not for "Wonderful": it starts lower.
|
||||
assert_eq!(lines.first(), Some(&"Wonderful"), "{composer:?}: {lines:?}");
|
||||
for li in &lines {
|
||||
assert!(li.split(' ').all(|w| text.split(' ').any(|word| word == w)), "{composer:?}: {lines:?}");
|
||||
}
|
||||
}
|
||||
// No line of this triangle is as wide as the word: it is still broken, and every letter is set.
|
||||
let word = "Supercalifragilisticexpialidocious";
|
||||
let l = layout(db(), &area_path(word, style(12.0), &triangle(60.0, 400.0), Justify::Left));
|
||||
assert!(!l.overflow && l.lines.len() > 1, "{:?}", line_places(&l));
|
||||
}
|
||||
|
||||
/// Lays `t` out as [`layout`] does, on a thread of its own, and fails the test when that panics or
|
||||
/// takes more than 20 s.
|
||||
fn layout_soon(t: &TextObject) -> TextLayout {
|
||||
|
|
|
|||
Loading…
Reference in New Issue