ailloli_ui_text/
wrap.rs

1//! Lightweight whitespace-normalizing word wrapping.
2
3use ailloli_ui_core::FontId;
4
5use crate::text_measure::TextMeasure;
6
7/// Word-wraps text without hyphenation.
8///
9/// `max_w_px` is a non-negative logical-pixel target; negative and NaN values
10/// behave as zero. Runs of Unicode whitespace, including explicit newlines,
11/// are normalized to one ASCII space within an output line. A word wider than
12/// the target is emitted intact, so this is not a hard width bound. The result
13/// always contains at least one line, with an empty string for whitespace-only
14/// input.
15///
16/// # Examples
17///
18/// ```
19/// use ailloli_ui_core::FontId;
20/// use ailloli_ui_text::{wrap_lines, ApproxTextMeasure};
21///
22/// let lines = wrap_lines("one   two three", 41.0, FontId::Ui, 10, &ApproxTextMeasure);
23/// assert_eq!(lines, ["one two", "three"]);
24/// assert_eq!(wrap_lines(" \n ", 20.0, FontId::Ui, 10, &ApproxTextMeasure), [""]);
25/// ```
26pub fn wrap_lines(
27    text: &str,
28    max_w_px: f32,
29    font: FontId,
30    px_size: u16,
31    m: &dyn TextMeasure,
32) -> Vec<String> {
33    let max_w_px = max_w_px.max(0.0);
34
35    let mut out = Vec::<String>::new();
36    let mut cur = String::new();
37    let mut cur_w = 0.0f32;
38
39    for word in text.split_whitespace() {
40        let word_w = m.measure(word, font, px_size);
41        let space_w = if cur.is_empty() {
42            0.0
43        } else {
44            m.advance(' ', font, px_size)
45        };
46
47        let next_w = cur_w + space_w + word_w;
48
49        if !cur.is_empty() && next_w > max_w_px {
50            out.push(std::mem::take(&mut cur));
51            cur_w = 0.0;
52        }
53
54        if !cur.is_empty() {
55            cur.push(' ');
56            cur_w += space_w;
57        }
58        cur.push_str(word);
59        cur_w += word_w;
60    }
61
62    if !cur.is_empty() {
63        out.push(cur);
64    }
65    if out.is_empty() {
66        out.push(String::new());
67    }
68    out
69}