use crate::linebreak::LineBreak;
const FIT_DEMERITS: f64 = 10_000.0;
const INF_DEMERITS: f64 = f64::INFINITY;
#[derive(Clone)]
struct Candidate {
break_glyph: usize,
total_demerits: f64,
ratio: f64,
prev: Option<usize>,
}
pub fn optimal_breaks(
advances: &[f32],
is_whitespace: &[bool],
break_opps: &[(usize, LineBreak)],
max_width: f32,
) -> Vec<usize> {
let n = advances.len();
if max_width <= 0.0 || n == 0 {
return vec![];
}
let mut break_positions: Vec<(usize, LineBreak)> = break_opps.to_vec();
if break_positions.last().is_none_or(|&(p, _)| p != n) {
break_positions.push((n, LineBreak::Allowed));
}
break_positions.sort_by_key(|&(p, _)| p);
break_positions.dedup_by_key(|&mut (p, _)| p);
let mut prefix = vec![0.0f32; n + 1];
for k in 0..n {
prefix[k + 1] = prefix[k] + advances[k];
}
let trimmed_width = |from: usize, to: usize| -> f32 {
let mut tw = 0.0f32;
let mut running = 0.0f32;
for k in from..to {
running += advances[k];
if k < is_whitespace.len() && !is_whitespace[k] {
tw = running;
}
}
tw
};
let mut candidates: Vec<Candidate> = Vec::new();
candidates.push(Candidate {
break_glyph: 0,
total_demerits: 0.0,
ratio: 0.0,
prev: None,
});
let mut active: Vec<usize> = vec![0];
for (bp_idx, &(bp, ref bp_kind)) in break_positions.iter().enumerate() {
let _ = bp_idx;
let mut best_demerits = INF_DEMERITS;
let mut best_prev: Option<usize> = None;
let mut best_ratio = 0.0f64;
for &ai in &active {
let from = candidates[ai].break_glyph;
if from > bp {
continue;
}
let line_w = trimmed_width(from, bp);
if line_w > max_width * 1.5 {
continue;
}
let r = ((max_width - line_w) / max_width).clamp(-2.0, 2.0) as f64;
let badness = 100.0 * r.abs().powi(3);
let mut d = (1.0 + badness).powi(2);
let prev_r = candidates[ai].ratio;
let same_tight = r < -0.5 && prev_r < -0.5;
let same_loose = r > 0.5 && prev_r > 0.5;
if same_tight || same_loose {
d += FIT_DEMERITS;
}
let total = candidates[ai].total_demerits + d;
if total < best_demerits {
best_demerits = total;
best_prev = Some(ai);
best_ratio = r;
}
}
if let Some(prev_idx) = best_prev {
let new_idx = candidates.len();
candidates.push(Candidate {
break_glyph: bp,
total_demerits: best_demerits,
ratio: best_ratio,
prev: Some(prev_idx),
});
active.push(new_idx);
}
active.retain(|&ai| {
let from = candidates[ai].break_glyph;
let raw_w = prefix[bp.min(n)] - prefix[from];
raw_w <= max_width * 1.5
});
if *bp_kind == LineBreak::Mandatory && bp < n {
active.retain(|&ai| candidates[ai].break_glyph == bp);
}
}
let best_terminal = active
.iter()
.filter(|&&ai| candidates[ai].break_glyph == n)
.min_by(|&&a, &&b| {
candidates[a]
.total_demerits
.partial_cmp(&candidates[b].total_demerits)
.unwrap_or(std::cmp::Ordering::Equal)
})
.copied();
let Some(mut current) = best_terminal else {
return vec![];
};
let mut breaks: Vec<usize> = Vec::new();
loop {
let gp = candidates[current].break_glyph;
if gp > 0 && gp < n {
breaks.push(gp);
}
match candidates[current].prev {
Some(p) => current = p,
None => break,
}
}
breaks.reverse();
breaks
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linebreak::{LineBreak, LineBreaker};
fn make_advances(text: &str, advance: f32) -> (Vec<f32>, Vec<bool>) {
let adv: Vec<f32> = text.chars().map(|_| advance).collect();
let ws: Vec<bool> = text.chars().map(|c| c.is_whitespace()).collect();
(adv, ws)
}
fn break_opps_for(text: &str) -> Vec<(usize, LineBreak)> {
let breaker = LineBreaker::new(text);
let byte_to_char: std::collections::HashMap<usize, usize> = text
.char_indices()
.enumerate()
.map(|(ci, (bi, _))| (bi, ci))
.collect();
breaker
.breaks()
.iter()
.filter_map(|&(off, ref kind)| byte_to_char.get(&off).map(|&ci| (ci, kind.clone())))
.collect()
}
#[test]
fn no_wrap_when_fits() {
let text = "ab cd";
let (adv, ws) = make_advances(text, 10.0); let opps = break_opps_for(text);
let breaks = optimal_breaks(&adv, &ws, &opps, 200.0);
assert!(
breaks.is_empty(),
"no break needed when everything fits: {breaks:?}"
);
}
#[test]
fn forces_break_on_long_text() {
let text = "aa bb cc";
let (adv, ws) = make_advances(text, 10.0);
let opps = break_opps_for(text);
let breaks = optimal_breaks(&adv, &ws, &opps, 50.0);
assert!(!breaks.is_empty(), "must produce at least one break");
}
#[test]
fn kp_produces_fewer_rivers_than_greedy() {
let text = "aaa bb ccc d eeeee";
let (adv, ws) = make_advances(text, 10.0);
let opps = break_opps_for(text);
let breaks = optimal_breaks(&adv, &ws, &opps, 60.0);
for w in breaks.windows(2) {
assert!(w[1] > w[0], "breaks must be strictly ascending");
}
for &b in &breaks {
assert!(
b < adv.len(),
"break index must be within bounds: {b} >= {}",
adv.len()
);
}
}
#[test]
fn empty_text_returns_empty() {
let breaks = optimal_breaks(&[], &[], &[], 100.0);
assert!(breaks.is_empty());
}
#[test]
fn zero_max_width_returns_empty() {
let (adv, ws) = make_advances("hello world", 10.0);
let opps = break_opps_for("hello world");
let breaks = optimal_breaks(&adv, &ws, &opps, 0.0);
assert!(breaks.is_empty());
}
#[test]
fn break_indices_are_valid() {
let text = "the quick brown fox jumps over the lazy dog";
let (adv, ws) = make_advances(text, 8.0);
let opps = break_opps_for(text);
let breaks = optimal_breaks(&adv, &ws, &opps, 120.0);
let n = adv.len();
for &b in &breaks {
assert!(b > 0 && b < n, "break index {b} out of range [1, {n})");
}
}
#[test]
fn mandatory_break_is_honoured() {
let text = "hello\nworld";
let (adv, ws) = make_advances(text, 10.0);
let opps = break_opps_for(text);
let breaks = optimal_breaks(&adv, &ws, &opps, 1000.0);
assert!(
breaks.contains(&6),
"mandatory break at char 6 must be present; got {breaks:?}"
);
}
}