1use super::FontId;
2use crate::{px, Line, Pixels, PlatformTextSystem, RunStyle, ShapedBoundary};
3use collections::HashMap;
4use std::{iter, sync::Arc};
5
6pub struct LineWrapper {
7 text_system: Arc<dyn PlatformTextSystem>,
8 pub(crate) font_id: FontId,
9 pub(crate) font_size: Pixels,
10 cached_ascii_char_widths: [Option<Pixels>; 128],
11 cached_other_char_widths: HashMap<char, Pixels>,
12}
13
14impl LineWrapper {
15 pub const MAX_INDENT: u32 = 256;
16
17 pub fn new(
18 font_id: FontId,
19 font_size: Pixels,
20 text_system: Arc<dyn PlatformTextSystem>,
21 ) -> Self {
22 Self {
23 text_system,
24 font_id,
25 font_size,
26 cached_ascii_char_widths: [None; 128],
27 cached_other_char_widths: HashMap::default(),
28 }
29 }
30
31 pub fn wrap_line<'a>(
32 &'a mut self,
33 line: &'a str,
34 wrap_width: Pixels,
35 ) -> impl Iterator<Item = Boundary> + 'a {
36 let mut width = px(0.);
37 let mut first_non_whitespace_ix = None;
38 let mut indent = None;
39 let mut last_candidate_ix = 0;
40 let mut last_candidate_width = px(0.);
41 let mut last_wrap_ix = 0;
42 let mut prev_c = '\0';
43 let mut char_indices = line.char_indices();
44 iter::from_fn(move || {
45 for (ix, c) in char_indices.by_ref() {
46 if c == '\n' {
47 continue;
48 }
49
50 if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
51 last_candidate_ix = ix;
52 last_candidate_width = width;
53 }
54
55 if c != ' ' && first_non_whitespace_ix.is_none() {
56 first_non_whitespace_ix = Some(ix);
57 }
58
59 let char_width = self.width_for_char(c);
60 width += char_width;
61 if width > wrap_width && ix > last_wrap_ix {
62 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
63 {
64 indent = Some(
65 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
66 );
67 }
68
69 if last_candidate_ix > 0 {
70 last_wrap_ix = last_candidate_ix;
71 width -= last_candidate_width;
72 last_candidate_ix = 0;
73 } else {
74 last_wrap_ix = ix;
75 width = char_width;
76 }
77
78 if let Some(indent) = indent {
79 width += self.width_for_char(' ') * indent as f32;
80 }
81
82 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
83 }
84 prev_c = c;
85 }
86
87 None
88 })
89 }
90
91 pub fn wrap_shaped_line<'a>(
92 &'a mut self,
93 str: &'a str,
94 line: &'a Line,
95 wrap_width: Pixels,
96 ) -> impl Iterator<Item = ShapedBoundary> + 'a {
97 let mut first_non_whitespace_ix = None;
98 let mut last_candidate_ix = None;
99 let mut last_candidate_x = px(0.);
100 let mut last_wrap_ix = ShapedBoundary {
101 run_ix: 0,
102 glyph_ix: 0,
103 };
104 let mut last_wrap_x = px(0.);
105 let mut prev_c = '\0';
106 let mut glyphs = line
107 .runs()
108 .iter()
109 .enumerate()
110 .flat_map(move |(run_ix, run)| {
111 run.glyphs()
112 .iter()
113 .enumerate()
114 .map(move |(glyph_ix, glyph)| {
115 let character = str[glyph.index..].chars().next().unwrap();
116 (
117 ShapedBoundary { run_ix, glyph_ix },
118 character,
119 glyph.position.x,
120 )
121 })
122 })
123 .peekable();
124
125 iter::from_fn(move || {
126 while let Some((ix, c, x)) = glyphs.next() {
127 if c == '\n' {
128 continue;
129 }
130
131 if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
132 last_candidate_ix = Some(ix);
133 last_candidate_x = x;
134 }
135
136 if c != ' ' && first_non_whitespace_ix.is_none() {
137 first_non_whitespace_ix = Some(ix);
138 }
139
140 let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
141 let width = next_x - last_wrap_x;
142 if width > wrap_width && ix > last_wrap_ix {
143 if let Some(last_candidate_ix) = last_candidate_ix.take() {
144 last_wrap_ix = last_candidate_ix;
145 last_wrap_x = last_candidate_x;
146 } else {
147 last_wrap_ix = ix;
148 last_wrap_x = x;
149 }
150
151 return Some(last_wrap_ix);
152 }
153 prev_c = c;
154 }
155
156 None
157 })
158 }
159
160 fn is_boundary(&self, prev: char, next: char) -> bool {
161 (prev == ' ') && (next != ' ')
162 }
163
164 #[inline(always)]
165 fn width_for_char(&mut self, c: char) -> Pixels {
166 if (c as u32) < 128 {
167 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
168 cached_width
169 } else {
170 let width = self.compute_width_for_char(c);
171 self.cached_ascii_char_widths[c as usize] = Some(width);
172 width
173 }
174 } else {
175 if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
176 *cached_width
177 } else {
178 let width = self.compute_width_for_char(c);
179 self.cached_other_char_widths.insert(c, width);
180 width
181 }
182 }
183 }
184
185 fn compute_width_for_char(&self, c: char) -> Pixels {
186 self.text_system
187 .layout_line(
188 &c.to_string(),
189 self.font_size,
190 &[(
191 1,
192 RunStyle {
193 font_id: self.font_id,
194 color: Default::default(),
195 underline: Default::default(),
196 },
197 )],
198 )
199 .width
200 }
201}
202
203#[derive(Copy, Clone, Debug, PartialEq, Eq)]
204pub struct Boundary {
205 pub ix: usize,
206 pub next_indent: u32,
207}
208
209impl Boundary {
210 fn new(ix: usize, next_indent: u32) -> Self {
211 Self { ix, next_indent }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::{AppContext, FontWeight};
219
220 #[test]
221 fn test_wrap_line() {
222 let cx = AppContext::test();
223
224 let text_system = cx.text_system().clone();
225 let family = text_system
226 .load_font_family(&["Courier"], &Default::default())
227 .unwrap();
228 let font_id = text_system
229 .select_font(family, Default::default(), Default::default())
230 .unwrap();
231
232 let mut wrapper =
233 LineWrapper::new(font_id, px(16.), text_system.platform_text_system.clone());
234 assert_eq!(
235 wrapper
236 .wrap_line("aa bbb cccc ddddd eeee", px(72.))
237 .collect::<Vec<_>>(),
238 &[
239 Boundary::new(7, 0),
240 Boundary::new(12, 0),
241 Boundary::new(18, 0)
242 ],
243 );
244 assert_eq!(
245 wrapper
246 .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
247 .collect::<Vec<_>>(),
248 &[
249 Boundary::new(4, 0),
250 Boundary::new(11, 0),
251 Boundary::new(18, 0)
252 ],
253 );
254 assert_eq!(
255 wrapper
256 .wrap_line(" aaaaaaa", px(72.))
257 .collect::<Vec<_>>(),
258 &[
259 Boundary::new(7, 5),
260 Boundary::new(9, 5),
261 Boundary::new(11, 5),
262 ]
263 );
264 assert_eq!(
265 wrapper
266 .wrap_line(" ", px(72.))
267 .collect::<Vec<_>>(),
268 &[
269 Boundary::new(7, 0),
270 Boundary::new(14, 0),
271 Boundary::new(21, 0)
272 ]
273 );
274 assert_eq!(
275 wrapper
276 .wrap_line(" aaaaaaaaaaaaaa", px(72.))
277 .collect::<Vec<_>>(),
278 &[
279 Boundary::new(7, 0),
280 Boundary::new(14, 3),
281 Boundary::new(18, 3),
282 Boundary::new(22, 3),
283 ]
284 );
285 }
286
287 // todo! repeat this test
288 #[test]
289 fn test_wrap_shaped_line() {
290 let cx = AppContext::test();
291 let text_system = cx.text_system().clone();
292
293 let family = text_system
294 .load_font_family(&["Helvetica"], &Default::default())
295 .unwrap();
296 let font_id = text_system
297 .select_font(family, Default::default(), Default::default())
298 .unwrap();
299 let normal = RunStyle {
300 font_id,
301 color: Default::default(),
302 underline: Default::default(),
303 };
304 let bold = RunStyle {
305 font_id: text_system
306 .select_font(family, FontWeight::BOLD, Default::default())
307 .unwrap(),
308 color: Default::default(),
309 underline: Default::default(),
310 };
311
312 let text = "aa bbb cccc ddddd eeee";
313 let line = text_system.layout_str(
314 text,
315 px(16.),
316 &[
317 (4, normal.clone()),
318 (5, bold.clone()),
319 (6, normal.clone()),
320 (1, bold),
321 (7, normal),
322 ],
323 );
324
325 let mut wrapper =
326 LineWrapper::new(font_id, px(16.), text_system.platform_text_system.clone());
327 assert_eq!(
328 wrapper
329 .wrap_shaped_line(text, &line, px(72.))
330 .collect::<Vec<_>>(),
331 &[
332 ShapedBoundary {
333 run_ix: 1,
334 glyph_ix: 3
335 },
336 ShapedBoundary {
337 run_ix: 2,
338 glyph_ix: 3
339 },
340 ShapedBoundary {
341 run_ix: 4,
342 glyph_ix: 2
343 }
344 ],
345 );
346 }
347}