Merge pull request #7413 from dotCooCoo/fix/line-break-latest-fitting

Type: a tabbed line that steps back ends at the latest break that fits as drawn
This commit is contained in:
BFlat
2026-11-15 00:41:19 +07:00
committed by GitHub
2 changed files with 72 additions and 14 deletions
+23 -14
View File
@@ -256,6 +256,11 @@ pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_do
/// back before bisecting the earlier ones.
const MAX_DRAWN_CHECKS: usize = 256;
/// Most breaks, or hyphenation points, [`break_line`] measures as drawn one by one when a glyph of
/// the line has a negative advance: a later break can then be drawn narrower than an earlier one,
/// and bisecting can miss the latest break that fits.
const MAX_DRAWN_CHECKS_STEPPING_BACK: usize = 1024;
/// Most copies of a leader drawn in one tab: a tiny type size and a far stop must not make
/// millions of outlines.
const MAX_LEADER_COPIES: f64 = 4096.0;
@@ -357,8 +362,9 @@ pub(super) fn word_width(g: &[SGlyph], i: usize, line_end: &[f64], kinsoku: Kins
/// from `i` ending before glyph `end` (with a hyphen when `hyphenated`) fits `width` as it is drawn:
/// once the line holds a tab, whose width depends on what is drawn before it, the line reads on
/// while the least it can be drawn at fits `width`, then ends at the latest legal break where it
/// fits as drawn (past [`MAX_DRAWN_CHECKS`] breaks back from the latest, the earlier ones without a
/// hyphen are bisected); hyphenation points are measured the same way.
/// fits as drawn (past [`MAX_DRAWN_CHECKS`] breaks back from the latest, or
/// [`MAX_DRAWN_CHECKS_STEPPING_BACK`] when a glyph of the line has a negative advance, the earlier
/// ones without a hyphen are bisected); hyphenation points are measured the same way.
#[allow(clippy::too_many_arguments)]
pub(super) fn break_line(
text: &str,
@@ -377,24 +383,26 @@ pub(super) fn break_line(
fits: Option<&dyn Fn(usize, bool) -> bool>,
) -> (usize, bool) {
// The line ending at `chosen`, once it holds a tab, when it fits as drawn; otherwise the latest
// of the legal `breaks` before it that fits as drawn, measuring at most MAX_DRAWN_CHECKS of
// them from the latest back and bisecting the earlier ones without a hyphen; the first of them
// when none of those measured fits.
let settle = |tabbed: bool, chosen: (usize, bool), breaks: &[(usize, bool)]| -> (usize, bool) {
// of the legal `breaks` before it that fits as drawn, measuring at most `checks` of them from
// the latest back and bisecting the earlier ones without a hyphen; the first of them when none
// of those measured fits.
let settle = |tabbed: bool, checks: usize, chosen: (usize, bool), breaks: &[(usize, bool)]| -> (usize, bool) {
let Some(fits) = fits.filter(|_| tabbed) else { return chosen };
if fits(chosen.0, chosen.1) {
return chosen;
}
let earlier = breaks.get(breaks.partition_point(|b| b.0 <= i)..breaks.partition_point(|b| b.0 < chosen.0)).unwrap_or_default();
if let Some(&b) = earlier.iter().rev().take(MAX_DRAWN_CHECKS).find(|b| fits(b.0, b.1)) {
if let Some(&b) = earlier.iter().rev().take(checks).find(|b| fits(b.0, b.1)) {
return b;
}
// Without a hyphen, a line that ends later is drawn no narrower, so those breaks bisect.
let rest = earlier.get(..earlier.len().saturating_sub(MAX_DRAWN_CHECKS)).unwrap_or_default();
let rest = earlier.get(..earlier.len().saturating_sub(checks)).unwrap_or_default();
let plain: Vec<&(usize, bool)> = rest.iter().filter(|b| !b.1).collect();
let fitting = plain.partition_point(|b| fits(b.0, b.1));
fitting.checked_sub(1).and_then(|k| plain.get(k).copied()).or(earlier.first()).copied().unwrap_or(chosen)
};
// How many breaks a line measures as drawn one by one.
let checks = |back: f64| if back > 0.0 { MAX_DRAWN_CHECKS_STEPPING_BACK } else { MAX_DRAWN_CHECKS };
let mut breaks: Vec<(usize, bool)> = Vec::new();
let mut tabbed = false;
if !width.is_finite() {
@@ -462,7 +470,7 @@ pub(super) fn break_line(
let cluster_ink = if is_tab { 0.0 } else { cluster_width };
let cluster_back: f64 = if is_tab { 0.0 } else { g[j..cluster_end].iter().map(|glyph| (-glyph.adv).max(0.0)).sum() };
if gl.ch == '\u{2028}' {
return settle(tabbed, (cluster_end, false), &breaks);
return settle(tabbed, checks(back), (cluster_end, false), &breaks);
}
// Measured as the line is set when it ends with this cluster: only the cluster's last glyph
// is narrowed, and only where the line may end.
@@ -509,7 +517,7 @@ pub(super) fn break_line(
j = cluster_end;
}
if j >= g.len() {
return settle(tabbed, (g.len(), false), &breaks);
return settle(tabbed, checks(back), (g.len(), false), &breaks);
}
// Hyphenate the word that overflows. A list marker starting the line is not part of a word.
let word_start = last_break.map_or(first, |b| b.0);
@@ -534,18 +542,19 @@ pub(super) fn break_line(
.filter(|&k| k > 0 && kinsoku_allows(g, k - 1, kinsoku) && !splits_no_break(g, k))
.collect();
// The points the line may end at, the latest first. Measured as drawn, at most
// MAX_DRAWN_CHECKS of them are measured one by one, and the earlier ones are bisected:
// `checks(back)` of them are measured one by one, and the earlier ones are bisected:
// each ends with a hyphen, so a later one is drawn no narrower.
let points: Vec<usize> = pts.iter().rev().copied().filter(|&k| k <= j && k > i).collect();
let drawn = fits.filter(|_| tabbed);
for &k in points.iter().take(if drawn.is_some() { MAX_DRAWN_CHECKS } else { usize::MAX }) {
let one_by_one = checks(back);
for &k in points.iter().take(if drawn.is_some() { one_by_one } else { usize::MAX }) {
let w = x_ws + g[word_start..k].iter().map(|g| g.adv).sum::<f64>() + hyphen_glyph(&g[k - 1]).adv;
if drawn.map_or(w <= width + EPS, |fits| fits(k, true)) {
return (k, true);
}
}
if let Some(fits) = drawn {
let earlier: Vec<usize> = points.iter().skip(MAX_DRAWN_CHECKS).rev().copied().collect();
let earlier: Vec<usize> = points.iter().skip(one_by_one).rev().copied().collect();
let fitting = earlier.partition_point(|&k| fits(k, true));
if let Some(&k) = fitting.checked_sub(1).and_then(|n| earlier.get(n)) {
return (k, true);
@@ -555,7 +564,7 @@ pub(super) fn break_line(
// Break candidates and greedy overflow are cluster boundaries; hyphenation also returns only
// cluster starts, so the resulting end always keeps each source cluster together.
match last_break {
Some(b) if b.0 > i => settle(tabbed, b, &breaks),
Some(b) if b.0 > i => settle(tabbed, checks(back), b, &breaks),
_ => (j, false),
}
}
+49
View File
@@ -646,6 +646,55 @@ fn a_negative_advance_after_a_tab_keeps_a_fitting_line_whole() {
assert_eq!(ended, (5, false));
}
/// A long tabbed line with a glyph that steps back ends at the latest break that fits as drawn, even
/// where a later break than one that does not fit fits again.
#[test]
fn a_long_tabbed_line_that_steps_back_ends_at_the_latest_break_that_fits_as_drawn() {
// a (0), tab (1), then b and a space for each u: breaks before 4 + 2u.
let text = format!("a\t{}", "b ".repeat(400));
let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), if byte == 2 { -0.5 } else { 1.0 }, ch)).collect();
let glyphs = synthetic_glyphs(&specs);
let drawn_fits = |end: usize, _hyphenated: bool| end <= 10 || end == 200;
let fits: Option<&dyn Fn(usize, bool) -> bool> = Some(&drawn_fits);
let ended =
break_line(&text, &glyphs, 0, 1e6, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false, &[], fits);
assert_eq!(ended, (200, false));
}
/// A tabbed line with a glyph that steps back, ending in a word with more hyphenation points than
/// are measured one by one, ends at the latest point that fits as drawn.
#[test]
fn a_tabbed_line_that_steps_back_ends_at_the_latest_hyphenation_point_that_fits_as_drawn() {
let opts = HyphenationOptions { words_longer_than: 0, after_first: 1, before_last: 1, ..Default::default() };
// a (0), tab (1), then a word of 400 syllables of two glyphs from glyph 2: points before 4, 6, ...
let text = format!("a\t{}", "कि".repeat(400));
let specs: Vec<_> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), if byte == 2 { -1.0 } else { 1.0 }, ch)).collect();
let mut glyphs = synthetic_glyphs(&specs);
for glyph in &mut glyphs {
glyph.hyphen_language = crate::hyphen::Language::from_tag(Some("hi"));
glyph.sx = 0.001;
}
let drawn_fits = |end: usize, _hyphenated: bool| end <= 20 || end == 50;
let fits: Option<&dyn Fn(usize, bool) -> bool> = Some(&drawn_fits);
let ended = break_line(
&text,
&glyphs,
0,
600.0,
&[],
0.0,
Some((&opts, 0.0)),
Burasagari::None,
Kinsoku::None,
KinsokuShoriType::PushOutOnly,
false,
false,
&[],
fits,
);
assert_eq!(ended, (50, true));
}
/// Mojikumi: the greedy breaker measures a line as it is set, with its last character narrowed
/// (`line_end`): 」 set half width ends a measure of three and a half characters. Only the last
/// glyph of the last cluster counts, and only where the line may end.