diff --git a/crates/text/src/composer.rs b/crates/text/src/composer.rs index 41b8b7f5c3..be738f639e 100644 --- a/crates/text/src/composer.rs +++ b/crates/text/src/composer.rs @@ -78,8 +78,9 @@ pub(crate) trait Measure { /// the search is to stop. fn line(&mut self, state: &Self::State, pos: usize, end: usize) -> Result, Exhausted>; /// Whether a line from `state` longer than `pos..end`, which [`Measure::line`] just measured, - /// may get more room than it. While it may, a line from `state` that is too long does not end - /// the reach of `state`. Lines from a state are all as wide by default. + /// may get more room than it. While it may, a line from `state` that is too long, or that + /// can't be set, does not end the reach of `state`. Lines from a state are all as wide by + /// default. fn may_widen(&mut self, _state: &Self::State, _pos: usize, _end: usize) -> bool { false } @@ -235,8 +236,15 @@ pub(crate) fn compose_with( // last glyph's advance, so a longer line from the same node is never narrower. let end = if hyphen || t <= a.pos { 0.0 } else { line_end_credit(t) }; let natural = w[t] - w[a.pos] - credit - end + bp.hyphen; - // A line that can't be set (no room left in the frame) ends this node's reach. - let Some((lw, next)) = measure.line(&a.state, a.pos, bp.end).ok()? else { continue }; + // A line that can't be set (no room left in the frame) ends this node's reach, unless a + // longer line may be placed elsewhere (a taller line steps down the frame by its own + // leading). + let Some((lw, next)) = measure.line(&a.state, a.pos, bp.end).ok()? else { + if measure.may_widen(&a.state, a.pos, bp.end) { + keep.push(ai); + } + continue; + }; // Adjustment ratio, badness and whether the line ends the active node's reach. let ratio = adjustment_ratio(natural, lw, st[t] - st[a.pos], sh[t] - sh[a.pos], last, p); let Some(ratio) = ratio.filter(|&r| r >= -1.0 || r.is_nan()) else { diff --git a/crates/text/src/layout/breaking.rs b/crates/text/src/layout/breaking.rs index 2eef2dd669..fd53a26428 100644 --- a/crates/text/src/layout/breaking.rs +++ b/crates/text/src/layout/breaking.rs @@ -110,9 +110,24 @@ impl Pen<'_> { let b = self.prev.map_or(0.0, |b| self.next_baseline(b, est)); return Some((b, f64::NEG_INFINITY, f64::INFINITY, false)); }; - if !self.queued.is_empty() { + // A further span was measured across the height of the line set before it on its baseline. + // This line gets the part of it that is free across its own height: a taller line can + // reach a wrap object the line before it stayed clear of. A span with no such part is + // left empty, and so is a part the line's height made narrower than `min`. + while !self.queued.is_empty() { let (b, x0, x1) = self.queued.remove(0); - return Some((b, x0, x1, true)); + let Some(r) = regions.get(self.ri) else { return Some((b, x0, x1, true)) }; + let mut free = r.spans(b - est.asc, b + est.desc).into_iter().filter_map(|(s, e)| { + let (s, e) = (s.max(x0), e.min(x1)); + let room = e - s - ind_l - ind_r; + let narrowed = s > x0 + EPS || e < x1 - EPS; + (room > est.asc.max(1.0) && (!narrowed || room + EPS >= min)).then_some((s, e)) + }); + let Some((s, e)) = free.next() else { continue }; + // An object inside the span splits it: the parts after the first stay queued. + let rest: Vec<_> = free.map(|(s, e)| (b, s, e)).collect(); + self.queued.splice(0..0, rest); + return Some((b, s, e, true)); } loop { let r = regions.get(self.ri)?; diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index 9195764211..9d54e6a1cc 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -611,6 +611,100 @@ fn composition_tries_a_longer_line_that_gets_more_room() { assert_eq!(lines, Some(vec![(7, false)])); } +/// A line measure for `compose_with` whose lines can't be set until they reach past glyph `from` +/// (a line made taller there steps down the frame by its own leading, to a band the shorter +/// line's steps miss), and are `wide` wide from there. +struct Unplaced { + wide: f64, + from: usize, +} + +impl crate::composer::Measure for Unplaced { + type State = usize; + fn first(&mut self) -> usize { + 0 + } + fn line(&mut self, _: &usize, _: usize, end: usize) -> Result, crate::composer::Exhausted> { + Ok((end > self.from).then_some((self.wide, end))) + } + fn may_widen(&mut self, _: &usize, _: usize, end: usize) -> bool { + end <= self.from + } +} + +/// A line that can't be set does not end the composer's search from its start while a longer line +/// may be placed elsewhere: the longer line is tried and set. +#[test] +fn composition_tries_a_longer_line_after_one_that_cannot_be_set() { + let text = "가나다 라마바"; + let glyphs = korean(text, false); + let cands = candidates(text, &glyphs, None, Kinsoku::Hard); + let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 }; + // "가나다" finds no room; the whole paragraph reaches past glyph 4 and fits 100. + let mut measure = Unplaced { wide: 100.0, from: 4 }; + let lines = crate::composer::compose_with(&glyphs, &mut measure, &|_| 0.0, &|_| 0.0, &cands, &[], ¶ms); + assert_eq!(lines, Some(vec![(7, false)])); +} + +/// A further span beside a wrap object goes to the line set in it with the room across that line's +/// own height. A line taller than the one before it on the baseline keeps clear of an object the +/// shorter line did not reach, and moves down when the span has no room for it. +#[test] +fn a_further_span_is_measured_across_the_height_of_its_line() { + use std::cell::Cell; + use std::rc::Rc; + let frame = Rect::new(0.0, 0.0, 400.0, 400.0).to_path(0.1); + let wrap = |r: Rect| vectorcraft_doc::WrapShape { + path: PathData::from_bezpath(&r.to_path(0.1)), + wrap: vectorcraft_doc::TextWrap { offset: 0.0, invert: false }, + }; + let small = breaking::Metrics { asc: 10.0, desc: 3.0, lead: 14.0, cap: 7.0, xh: 5.0, top: 10.0 }; + let tall = breaking::Metrics { asc: 30.0, desc: 9.0, lead: 42.0, cap: 21.0, xh: 15.0, top: 30.0 }; + // An object down the middle of the frame splits every line in two, and `above` lies right of + // it over y 20 to 40. A small line after a baseline of 50 is set at 64, over y 54 to 67, clear + // of `above`; a tall line in the span right of it on that baseline covers y 34 to 73. + // The tall line starts with a word `word` wide. + let set = |above: Rect, word: f64| { + let regions = region::Region::cells(&frame, &LayoutOptions::default(), &[wrap(Rect::new(180.0, 0.0, 220.0, 400.0)), wrap(above)]); + let mut pen = breaking::Pen { + regions: Some(®ions), + ri: 0, + prev: Some(50.0), + pending: 0.0, + fb: FirstBaseline::Ascent, + fb_min: 0.0, + queued: vec![], + model: LeadingModel::RomanBaseline, + next_top: None, + no_room_for: f64::INFINITY, + steps_left: Rc::new(Cell::new(breaking::MAX_TOTAL_STEPS)), + search: Default::default(), + }; + let left = pen.place(small, 0.0, 0.0, 0.0); + pen.settled(64.0, small); + let right = pen.place(tall, 0.0, 0.0, word); + (left, right) + }; + let near = |got: Option<(f64, f64, f64, bool)>, want: (f64, f64, f64, bool)| { + got.is_some_and(|g| (g.0 - want.0).abs() < 0.01 && (g.1 - want.1).abs() < 0.01 && (g.2 - want.2).abs() < 0.01 && g.3 == want.3) + }; + // `above` covers the right end of the span: the tall line gets the part left of it. + let (left, right) = set(Rect::new(300.0, 20.0, 400.0, 40.0), 0.0); + assert!(near(left, (64.0, 0.0, 180.0, false)), "{left:?}"); + assert!(near(right, (64.0, 220.0, 300.0, true)), "{right:?}"); + // That part is 80 wide: a tall line that starts with a wider word goes on the next baseline. + let (_, right) = set(Rect::new(300.0, 20.0, 400.0, 40.0), 100.0); + assert!(near(right, (106.0, 0.0, 180.0, false)), "{right:?}"); + // `above` covers the whole span: the tall line goes on the next baseline, left of the object. + let (left, right) = set(Rect::new(220.0, 20.0, 400.0, 40.0), 0.0); + assert!(near(left, (64.0, 0.0, 180.0, false)), "{left:?}"); + assert!(near(right, (106.0, 0.0, 180.0, false)), "{right:?}"); + // `above` lies below the tall line: the span is as wide for it as for the small line, and it + // takes the span whatever its first word, as a further span always did. + let (_, right) = set(Rect::new(300.0, 300.0, 400.0, 320.0), 200.0); + assert!(near(right, (64.0, 220.0, 400.0, true)), "{right:?}"); +} + /// The every-line composer's searches for the paragraphs of one frame share one budget: each takes /// the lines it tried and the baseline steps it spent from it, the layout's own steps stay as they /// were, and once the budget runs out the next paragraph's search stops (it is broken line by line).