use crate::face::Face;
use crate::shaper::{PositionedGlyph, Shaper};
use crate::Error;
pub fn run_width(glyphs: &[PositionedGlyph]) -> f32 {
let mut w = 0.0;
for g in glyphs {
w += g.x_advance + g.x_offset;
}
w
}
pub fn wrap_lines(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
) -> Result<Vec<String>, Error> {
if text.is_empty() {
return Ok(Vec::new());
}
if max_width <= 0.0 {
return Ok(text.split('\n').map(|s| s.to_string()).collect());
}
let mut lines: Vec<String> = Vec::new();
for paragraph in text.split('\n') {
wrap_paragraph(face, paragraph, size_px, max_width, &mut lines)?;
}
Ok(lines)
}
fn wrap_paragraph(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
lines: &mut Vec<String>,
) -> Result<(), Error> {
if text.is_empty() {
lines.push(String::new());
return Ok(());
}
let words: Vec<String> = split_keeping_whitespace(text);
if words.is_empty() {
lines.push(text.to_string());
return Ok(());
}
let mut current = String::new();
for word in words {
let candidate = if current.is_empty() {
word.trim_start().to_string()
} else {
format!("{current}{word}")
};
let glyphs = Shaper::shape(face, &candidate, size_px)?;
if run_width(&glyphs) <= max_width || current.is_empty() {
current = candidate;
let cur_glyphs = Shaper::shape(face, ¤t, size_px)?;
if run_width(&cur_glyphs) > max_width {
let (head, tail) = hard_break(face, ¤t, size_px, max_width)?;
lines.push(head);
current = tail;
}
} else {
lines.push(current.clone());
current = word.trim_start().to_string();
}
}
if !current.is_empty() {
lines.push(current);
}
Ok(())
}
fn split_keeping_whitespace(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut buf = String::new();
let mut in_word = false;
for ch in s.chars() {
if ch.is_whitespace() {
if in_word {
out.push(std::mem::take(&mut buf));
in_word = false;
}
buf.push(ch);
} else {
in_word = true;
buf.push(ch);
}
}
if !buf.is_empty() {
out.push(buf);
}
out
}
fn hard_break(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
) -> Result<(String, String), Error> {
let chars: Vec<char> = text.chars().collect();
let mut last_good = 0usize;
for n in 1..=chars.len() {
let candidate: String = chars[..n].iter().collect();
let glyphs = Shaper::shape(face, &candidate, size_px)?;
if run_width(&glyphs) > max_width {
break;
}
last_good = n;
}
if last_good == 0 {
last_good = 1.min(chars.len());
}
let head: String = chars[..last_good].iter().collect();
let tail: String = chars[last_good..].iter().collect();
Ok((head, tail))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_keeping_whitespace_basic() {
let v = split_keeping_whitespace("hello world foo");
assert_eq!(v, vec!["hello", " world", " foo"]);
}
#[test]
fn split_keeping_whitespace_leading_trailing() {
let v = split_keeping_whitespace(" hi");
assert_eq!(v, vec![" hi"]);
}
#[test]
fn empty_text_is_empty_lines() {
}
}