use crate::font::Font;
use crate::ledger::consts::TEXT_LEADING;
pub fn approx_width(text: &str, font: Font, font_size: f64, letter_spacing: f64) -> f64 {
text.split('\n')
.map(|line| line_width(line, font, font_size, letter_spacing))
.fold(0.0_f64, f64::max)
}
fn line_width(line: &str, font: Font, font_size: f64, letter_spacing: f64) -> f64 {
let mut chars = line.chars();
let Some(first) = chars.next() else {
return 0.0;
};
let first_em = font.advance_em(first);
let mut n = 1usize;
let mut uniform = true;
let mut pos = first_em * font_size;
let mut extent = first_em * font_size;
for ch in chars {
n += 1;
let em = font.advance_em(ch);
uniform &= em == first_em;
let advance = em * font_size;
pos += letter_spacing;
extent = extent.max(pos.abs() + advance);
pos += advance;
}
if uniform {
let advance = first_em * font_size;
let step = (advance + letter_spacing).abs();
return advance + (n as f64 - 1.0) * step;
}
extent
}
const SINGLE_LINE_EM: f64 = 1.0;
pub fn approx_height(text: &str, font_size: f64, line_spacing: f64) -> f64 {
let line_count = text.split('\n').count().max(1);
if line_count == 1 {
return SINGLE_LINE_EM * font_size;
}
let leading = TEXT_LEADING * font_size;
let step = (leading + line_spacing).abs();
leading + (line_count as f64 - 1.0) * step
}
pub fn wrap(
text: &str,
font: Font,
font_size: f64,
letter_spacing: f64,
max_w: f64,
) -> Vec<String> {
let fits = |s: &str| approx_width(s, font, font_size, letter_spacing) <= max_w + 1e-9;
let mut out = Vec::new();
for raw in text.split('\n') {
let mut cur = String::new();
for word in raw.split_whitespace() {
let candidate = if cur.is_empty() {
word.to_string()
} else {
format!("{cur} {word}")
};
if fits(&candidate) {
cur = candidate;
continue;
}
if !cur.is_empty() {
out.push(std::mem::take(&mut cur));
}
let mut rest: &str = word;
while !fits(rest) && rest.chars().count() > 1 {
let mut cut = rest.chars().next().map_or(1, char::len_utf8);
for (i, _) in rest.char_indices().skip(1) {
if fits(&rest[..i]) {
cut = i;
} else {
break;
}
}
out.push(rest[..cut].to_string());
rest = &rest[cut..];
}
cur = rest.to_string();
}
out.push(cur);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const F: Font = Font::MONO_REGULAR;
#[test]
fn single_line_width_scales_with_chars_and_size() {
let w = approx_width("hello", F, 13.0, 0.0);
assert!((w - 39.0).abs() < 0.01, "got {}", w);
}
#[test]
fn multi_line_picks_widest() {
let w = approx_width("hi\nhello", F, 10.0, 0.0);
assert!((w - 30.0).abs() < 0.01, "got {}", w);
}
#[test]
fn letter_spacing_widens_by_the_gaps_only() {
assert!((approx_width("hello", F, 13.0, 4.0) - 55.0).abs() < 0.01);
assert!((approx_width("x", F, 13.0, 4.0) - approx_width("x", F, 13.0, 0.0)).abs() < 0.01);
}
#[test]
fn height_grows_with_lines() {
let h1 = approx_height("a", 10.0, 0.0);
let h2 = approx_height("a\nb", 10.0, 0.0);
assert!((h1 - 10.0).abs() < 0.01, "got {h1}");
assert!((h2 - 24.0).abs() < 0.01, "got {h2}");
}
#[test]
fn line_spacing_adds_between_lines_only() {
assert!((approx_height("a\nb", 10.0, 6.0) - 30.0).abs() < 0.01);
assert!((approx_height("a", 10.0, 6.0) - approx_height("a", 10.0, 0.0)).abs() < 0.01);
}
#[test]
fn wrap_prefers_whitespace() {
let lines = wrap("wrap me into some lines", F, 10.0, 0.0, 60.0);
assert_eq!(lines, ["wrap me", "into some", "lines"]);
}
#[test]
fn wrap_breaks_inside_an_oversized_word() {
let lines = wrap("abcdefghijkl", F, 10.0, 0.0, 30.0);
assert_eq!(lines, ["abcde", "fghij", "kl"]);
}
#[test]
fn wrap_never_goes_below_one_glyph() {
let lines = wrap("abc", F, 10.0, 0.0, 1.0);
assert_eq!(lines, ["a", "b", "c"]);
}
#[test]
fn wrap_respects_authored_newlines() {
let lines = wrap("one two\nthree", F, 10.0, 0.0, 30.0);
assert_eq!(lines, ["one", "two", "three"]);
assert_eq!(wrap("a\n\nb", F, 10.0, 0.0, 60.0), ["a", "", "b"]);
}
#[test]
fn wrapped_size_is_the_measured_size() {
let joined = wrap("wrap me into some lines", F, 10.0, 0.0, 60.0).join("\n");
assert!(approx_width(&joined, F, 10.0, 0.0) <= 60.0);
assert!(approx_height(&joined, 10.0, 0.0) > approx_height("x", 10.0, 0.0));
}
#[test]
fn negative_letter_spacing_collapses_then_grows_back() {
let advance = 9.0;
assert!((approx_width("hello", F, 15.0, -9.0) - advance).abs() < 0.01);
assert!((approx_width("hello", F, 15.0, -18.0) - 45.0).abs() < 0.01);
assert!(approx_width("hello", F, 15.0, -1000.0) >= advance);
}
#[test]
fn negative_line_spacing_collapses_then_grows_back() {
let leading = 12.0;
assert!((approx_height("a\nb\nc", 10.0, -12.0) - leading).abs() < 0.01);
assert!(approx_height("a\nb\nc", 10.0, -1000.0) >= leading);
}
}