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}