1use crate::face::Face;
11use crate::shaper::{PositionedGlyph, Shaper};
12use crate::Error;
13
14pub fn run_width(glyphs: &[PositionedGlyph]) -> f32 {
18 let mut w = 0.0;
19 for g in glyphs {
20 w += g.x_advance + g.x_offset;
21 }
22 w
23}
24
25pub fn wrap_lines(
34 face: &Face,
35 text: &str,
36 size_px: f32,
37 max_width: f32,
38) -> Result<Vec<String>, Error> {
39 if text.is_empty() {
40 return Ok(Vec::new());
41 }
42 if max_width <= 0.0 {
43 return Ok(text.split('\n').map(|s| s.to_string()).collect());
47 }
48
49 let mut lines: Vec<String> = Vec::new();
50 for paragraph in text.split('\n') {
51 wrap_paragraph(face, paragraph, size_px, max_width, &mut lines)?;
52 }
53 Ok(lines)
54}
55
56fn wrap_paragraph(
57 face: &Face,
58 text: &str,
59 size_px: f32,
60 max_width: f32,
61 lines: &mut Vec<String>,
62) -> Result<(), Error> {
63 if text.is_empty() {
64 lines.push(String::new());
65 return Ok(());
66 }
67
68 let words: Vec<String> = split_keeping_whitespace(text);
71 if words.is_empty() {
72 lines.push(text.to_string());
73 return Ok(());
74 }
75
76 let mut current = String::new();
77 for word in words {
78 let candidate = if current.is_empty() {
79 word.trim_start().to_string()
80 } else {
81 format!("{current}{word}")
82 };
83 let glyphs = Shaper::shape(face, &candidate, size_px)?;
84 if run_width(&glyphs) <= max_width || current.is_empty() {
85 current = candidate;
86 let cur_glyphs = Shaper::shape(face, ¤t, size_px)?;
88 if run_width(&cur_glyphs) > max_width {
89 let (head, tail) = hard_break(face, ¤t, size_px, max_width)?;
90 lines.push(head);
91 current = tail;
92 }
93 } else {
94 lines.push(current.clone());
95 current = word.trim_start().to_string();
96 }
97 }
98 if !current.is_empty() {
99 lines.push(current);
100 }
101 Ok(())
102}
103
104fn split_keeping_whitespace(s: &str) -> Vec<String> {
108 let mut out: Vec<String> = Vec::new();
109 let mut buf = String::new();
110 let mut in_word = false;
111 for ch in s.chars() {
112 if ch.is_whitespace() {
113 if in_word {
114 out.push(std::mem::take(&mut buf));
115 in_word = false;
116 }
117 buf.push(ch);
118 } else {
119 in_word = true;
120 buf.push(ch);
121 }
122 }
123 if !buf.is_empty() {
124 out.push(buf);
125 }
126 out
127}
128
129fn hard_break(
132 face: &Face,
133 text: &str,
134 size_px: f32,
135 max_width: f32,
136) -> Result<(String, String), Error> {
137 let chars: Vec<char> = text.chars().collect();
138 let mut last_good = 0usize;
139 for n in 1..=chars.len() {
140 let candidate: String = chars[..n].iter().collect();
141 let glyphs = Shaper::shape(face, &candidate, size_px)?;
142 if run_width(&glyphs) > max_width {
143 break;
144 }
145 last_good = n;
146 }
147 if last_good == 0 {
148 last_good = 1.min(chars.len());
151 }
152 let head: String = chars[..last_good].iter().collect();
153 let tail: String = chars[last_good..].iter().collect();
154 Ok((head, tail))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn split_keeping_whitespace_basic() {
163 let v = split_keeping_whitespace("hello world foo");
164 assert_eq!(v, vec!["hello", " world", " foo"]);
165 }
166
167 #[test]
168 fn split_keeping_whitespace_leading_trailing() {
169 let v = split_keeping_whitespace(" hi");
170 assert_eq!(v, vec![" hi"]);
171 }
172
173 #[test]
174 fn empty_text_is_empty_lines() {
175 }
181}