mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user