1use std::{collections::VecDeque, mem};
2
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6use crate::{layout::Alignment, text::StyledGrapheme};
7
8pub trait LineComposer<'a> {
12 fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>>;
13}
14
15pub struct WrappedLine<'lend, 'text> {
16 pub line: &'lend [StyledGrapheme<'text>],
18 pub width: u16,
20 pub alignment: Alignment,
22}
23
24#[derive(Debug, Default, Clone)]
26pub struct WordWrapper<'a, O, I>
27where
28 O: Iterator<Item = (I, Alignment)>,
30 I: Iterator<Item = StyledGrapheme<'a>>,
33{
34 input_lines: O,
36 max_line_width: u16,
37 wrapped_lines: VecDeque<Vec<StyledGrapheme<'a>>>,
38 current_alignment: Alignment,
39 current_line: Vec<StyledGrapheme<'a>>,
40 trim: bool,
42
43 pending_word: Vec<StyledGrapheme<'a>>,
45 pending_whitespace: VecDeque<StyledGrapheme<'a>>,
46 pending_line_pool: Vec<Vec<StyledGrapheme<'a>>>,
47}
48
49impl<'a, O, I> WordWrapper<'a, O, I>
50where
51 O: Iterator<Item = (I, Alignment)>,
52 I: Iterator<Item = StyledGrapheme<'a>>,
53{
54 pub const fn new(lines: O, max_line_width: u16, trim: bool) -> Self {
55 Self {
56 input_lines: lines,
57 max_line_width,
58 wrapped_lines: VecDeque::new(),
59 current_alignment: Alignment::Left,
60 current_line: vec![],
61 trim,
62
63 pending_word: Vec::new(),
64 pending_line_pool: Vec::new(),
65 pending_whitespace: VecDeque::new(),
66 }
67 }
68
69 fn process_input(&mut self, line_symbols: impl IntoIterator<Item = StyledGrapheme<'a>>) {
72 let mut pending_line = self.pending_line_pool.pop().unwrap_or_default();
73 let mut line_width = 0;
74 let mut word_width = 0;
75 let mut whitespace_width = 0;
76 let mut non_whitespace_previous = false;
77
78 self.pending_word.clear();
79 self.pending_whitespace.clear();
80 pending_line.clear();
81
82 for grapheme in line_symbols {
83 let is_whitespace = grapheme.is_whitespace();
84 let symbol_width = grapheme.symbol.width() as u16;
85
86 if symbol_width > self.max_line_width {
88 continue;
89 }
90
91 let word_found = non_whitespace_previous && is_whitespace;
92 let trimmed_overflow = pending_line.is_empty()
94 && self.trim
95 && word_width + symbol_width > self.max_line_width;
96 let whitespace_overflow = pending_line.is_empty()
98 && self.trim
99 && whitespace_width + symbol_width > self.max_line_width;
100 let untrimmed_overflow = pending_line.is_empty()
102 && !self.trim
103 && word_width + whitespace_width + symbol_width > self.max_line_width;
104
105 if word_found || trimmed_overflow || whitespace_overflow || untrimmed_overflow {
107 if !pending_line.is_empty() || !self.trim {
108 pending_line.extend(self.pending_whitespace.drain(..));
109 line_width += whitespace_width;
110 }
111
112 pending_line.append(&mut self.pending_word);
113 line_width += word_width;
114
115 self.pending_whitespace.clear();
116 whitespace_width = 0;
117 word_width = 0;
118 }
119
120 let line_full = line_width >= self.max_line_width;
122 let pending_word_overflow = symbol_width > 0
124 && line_width + whitespace_width + word_width >= self.max_line_width;
125
126 if line_full || pending_word_overflow {
128 let mut remaining_width = u16::saturating_sub(self.max_line_width, line_width);
129
130 self.wrapped_lines.push_back(mem::take(&mut pending_line));
131 line_width = 0;
132
133 while let Some(grapheme) = self.pending_whitespace.front() {
135 let width = grapheme.symbol.width() as u16;
136
137 if width > remaining_width {
138 break;
139 }
140
141 whitespace_width -= width;
142 remaining_width -= width;
143 self.pending_whitespace.pop_front();
144 }
145
146 if is_whitespace && self.pending_whitespace.is_empty() {
148 continue;
149 }
150 }
151
152 if is_whitespace {
154 whitespace_width += symbol_width;
155 self.pending_whitespace.push_back(grapheme);
156 } else {
157 word_width += symbol_width;
158 self.pending_word.push(grapheme);
159 }
160
161 non_whitespace_previous = !is_whitespace;
162 }
163
164 if pending_line.is_empty()
166 && self.pending_word.is_empty()
167 && !self.pending_whitespace.is_empty()
168 {
169 self.wrapped_lines.push_back(vec![]);
170 }
171 if !pending_line.is_empty() || !self.trim {
172 pending_line.extend(self.pending_whitespace.drain(..));
173 }
174 pending_line.append(&mut self.pending_word);
175
176 #[allow(clippy::else_if_without_else)]
177 if !pending_line.is_empty() {
178 self.wrapped_lines.push_back(pending_line);
179 } else if pending_line.capacity() > 0 {
180 self.pending_line_pool.push(pending_line);
181 }
182 if self.wrapped_lines.is_empty() {
183 self.wrapped_lines.push_back(vec![]);
184 }
185 }
186
187 fn replace_current_line(&mut self, line: Vec<StyledGrapheme<'a>>) {
188 let cache = mem::replace(&mut self.current_line, line);
189 if cache.capacity() > 0 {
190 self.pending_line_pool.push(cache);
191 }
192 }
193}
194
195impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
196where
197 O: Iterator<Item = (I, Alignment)>,
198 I: Iterator<Item = StyledGrapheme<'a>>,
199{
200 #[allow(clippy::too_many_lines)]
201 fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
202 if self.max_line_width == 0 {
203 return None;
204 }
205
206 loop {
207 if let Some(line) = self.wrapped_lines.pop_front() {
209 let line_width = line
210 .iter()
211 .map(|grapheme| grapheme.symbol.width() as u16)
212 .sum();
213
214 self.replace_current_line(line);
215 return Some(WrappedLine {
216 line: &self.current_line,
217 width: line_width,
218 alignment: self.current_alignment,
219 });
220 }
221
222 let (line_symbols, line_alignment) = self.input_lines.next()?;
224 self.current_alignment = line_alignment;
225 self.process_input(line_symbols);
226 }
227 }
228}
229
230#[derive(Debug, Default, Clone)]
232pub struct LineTruncator<'a, O, I>
233where
234 O: Iterator<Item = (I, Alignment)>,
236 I: Iterator<Item = StyledGrapheme<'a>>,
239{
240 input_lines: O,
242 max_line_width: u16,
243 current_line: Vec<StyledGrapheme<'a>>,
244 horizontal_offset: u16,
246}
247
248impl<'a, O, I> LineTruncator<'a, O, I>
249where
250 O: Iterator<Item = (I, Alignment)>,
251 I: Iterator<Item = StyledGrapheme<'a>>,
252{
253 pub const fn new(lines: O, max_line_width: u16) -> Self {
254 Self {
255 input_lines: lines,
256 max_line_width,
257 horizontal_offset: 0,
258 current_line: vec![],
259 }
260 }
261
262 pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
263 self.horizontal_offset = horizontal_offset;
264 }
265}
266
267impl<'a, O, I> LineComposer<'a> for LineTruncator<'a, O, I>
268where
269 O: Iterator<Item = (I, Alignment)>,
270 I: Iterator<Item = StyledGrapheme<'a>>,
271{
272 fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
273 if self.max_line_width == 0 {
274 return None;
275 }
276
277 self.current_line.truncate(0);
278 let mut current_line_width = 0;
279
280 let mut lines_exhausted = true;
281 let mut horizontal_offset = self.horizontal_offset as usize;
282 let mut current_alignment = Alignment::Left;
283 if let Some((current_line, alignment)) = &mut self.input_lines.next() {
284 lines_exhausted = false;
285 current_alignment = *alignment;
286
287 for StyledGrapheme { symbol, style } in current_line {
288 if symbol.width() as u16 > self.max_line_width {
290 continue;
291 }
292
293 if current_line_width + symbol.width() as u16 > self.max_line_width {
294 break;
296 }
297
298 let symbol = if horizontal_offset == 0 || Alignment::Left != *alignment {
299 symbol
300 } else {
301 let w = symbol.width();
302 if w > horizontal_offset {
303 let t = trim_offset(symbol, horizontal_offset);
304 horizontal_offset = 0;
305 t
306 } else {
307 horizontal_offset -= w;
308 ""
309 }
310 };
311 current_line_width += symbol.width() as u16;
312 self.current_line.push(StyledGrapheme { symbol, style });
313 }
314 }
315
316 if lines_exhausted {
317 None
318 } else {
319 Some(WrappedLine {
320 line: &self.current_line,
321 width: current_line_width,
322 alignment: current_alignment,
323 })
324 }
325 }
326}
327
328fn trim_offset(src: &str, mut offset: usize) -> &str {
331 let mut start = 0;
332 for c in UnicodeSegmentation::graphemes(src, true) {
333 let w = c.width();
334 if w <= offset {
335 offset -= w;
336 start += c.len();
337 } else {
338 break;
339 }
340 }
341 #[allow(clippy::string_slice)] &src[start..]
343}
344
345#[cfg(test)]
346mod test {
347 use super::*;
348 use crate::{
349 style::Style,
350 text::{Line, Text},
351 };
352
353 #[derive(Clone, Copy)]
354 enum Composer {
355 WordWrapper { trim: bool },
356 LineTruncator,
357 }
358
359 fn run_composer<'a>(
360 which: Composer,
361 text: impl Into<Text<'a>>,
362 text_area_width: u16,
363 ) -> (Vec<String>, Vec<u16>, Vec<Alignment>) {
364 let text = text.into();
365 let styled_lines = text.iter().map(|line| {
366 (
367 line.iter()
368 .flat_map(|span| span.styled_graphemes(Style::default())),
369 line.alignment.unwrap_or(Alignment::Left),
370 )
371 });
372
373 let mut composer: Box<dyn LineComposer> = match which {
374 Composer::WordWrapper { trim } => {
375 Box::new(WordWrapper::new(styled_lines, text_area_width, trim))
376 }
377 Composer::LineTruncator => Box::new(LineTruncator::new(styled_lines, text_area_width)),
378 };
379 let mut lines = vec![];
380 let mut widths = vec![];
381 let mut alignments = vec![];
382 while let Some(WrappedLine {
383 line: styled,
384 width,
385 alignment,
386 }) = composer.next_line()
387 {
388 let line = styled
389 .iter()
390 .map(|StyledGrapheme { symbol, .. }| *symbol)
391 .collect::<String>();
392 assert!(width <= text_area_width);
393 lines.push(line);
394 widths.push(width);
395 alignments.push(alignment);
396 }
397 (lines, widths, alignments)
398 }
399
400 #[test]
401 fn line_composer_one_line() {
402 let width = 40;
403 for i in 1..width {
404 let text = "a".repeat(i);
405 let (word_wrapper, _, _) =
406 run_composer(Composer::WordWrapper { trim: true }, &*text, width as u16);
407 let (line_truncator, _, _) =
408 run_composer(Composer::LineTruncator, &*text, width as u16);
409 let expected = vec![text];
410 assert_eq!(word_wrapper, expected);
411 assert_eq!(line_truncator, expected);
412 }
413 }
414
415 #[test]
416 fn line_composer_short_lines() {
417 let width = 20;
418 let text =
419 "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
420 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
421 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
422
423 let wrapped: Vec<&str> = text.split('\n').collect();
424 assert_eq!(word_wrapper, wrapped);
425 assert_eq!(line_truncator, wrapped);
426 }
427
428 #[test]
429 fn line_composer_long_word() {
430 let width = 20;
431 let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
432 let (word_wrapper, _, _) =
433 run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
434 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
435
436 let wrapped = vec![
437 text.get(..width).unwrap(),
438 text.get(width..width * 2).unwrap(),
439 text.get(width * 2..width * 3).unwrap(),
440 text.get(width * 3..).unwrap(),
441 ];
442 assert_eq!(
443 word_wrapper, wrapped,
444 "WordWrapper should detect the line cannot be broken on word boundary and \
445 break it at line width limit."
446 );
447 assert_eq!(line_truncator, [text.get(..width).unwrap()]);
448 }
449
450 #[test]
451 fn line_composer_long_sentence() {
452 let width = 20;
453 let text =
454 "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
455 let text_multi_space =
456 "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
457 m n o";
458 let (word_wrapper_single_space, _, _) =
459 run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
460 let (word_wrapper_multi_space, _, _) = run_composer(
461 Composer::WordWrapper { trim: true },
462 text_multi_space,
463 width as u16,
464 );
465 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
466
467 let word_wrapped = vec![
468 "abcd efghij",
469 "klmnopabcd efgh",
470 "ijklmnopabcdefg",
471 "hijkl mnopab c d e f",
472 "g h i j k l m n o",
473 ];
474 assert_eq!(word_wrapper_single_space, word_wrapped);
475 assert_eq!(word_wrapper_multi_space, word_wrapped);
476
477 assert_eq!(line_truncator, [text.get(..width).unwrap()]);
478 }
479
480 #[test]
481 fn line_composer_zero_width() {
482 let width = 0;
483 let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
484 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
485 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
486
487 let expected: Vec<&str> = Vec::new();
488 assert_eq!(word_wrapper, expected);
489 assert_eq!(line_truncator, expected);
490 }
491
492 #[test]
493 fn line_composer_max_line_width_of_1() {
494 let width = 1;
495 let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
496 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
497 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
498
499 let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
500 .filter(|g| g.chars().any(|c| !c.is_whitespace()))
501 .collect();
502 assert_eq!(word_wrapper, expected);
503 assert_eq!(line_truncator, ["a"]);
504 }
505
506 #[test]
507 fn line_composer_max_line_width_of_1_double_width_characters() {
508 let width = 1;
509 let text =
510 "コンピュータ上で文字を扱う場合、典型的には文字\naaa\naによる通信を行う場合にその\
511 両端点では、";
512 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
513 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
514 assert_eq!(word_wrapper, ["", "a", "a", "a", "a"]);
515 assert_eq!(line_truncator, ["", "a", "a"]);
516 }
517
518 #[test]
520 fn line_composer_word_wrapper_mixed_length() {
521 let width = 20;
522 let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
523 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
524 assert_eq!(
525 word_wrapper,
526 vec![
527 "abcd efghij",
528 "klmnopabcdefghijklmn",
529 "opabcdefghijkl",
530 "mnopab cdefghi j",
531 "klmno",
532 ]
533 );
534 }
535
536 #[test]
537 fn line_composer_double_width_chars() {
538 let width = 20;
539 let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
540 では、";
541 let (word_wrapper, word_wrapper_width, _) =
542 run_composer(Composer::WordWrapper { trim: true }, text, width);
543 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
544 assert_eq!(line_truncator, ["コンピュータ上で文字"]);
545 let wrapped = [
546 "コンピュータ上で文字",
547 "を扱う場合、典型的に",
548 "は文字による通信を行",
549 "う場合にその両端点で",
550 "は、",
551 ];
552 assert_eq!(word_wrapper, wrapped);
553 assert_eq!(word_wrapper_width, [width, width, width, width, 4]);
554 }
555
556 #[test]
557 fn line_composer_leading_whitespace_removal() {
558 let width = 20;
559 let text = "AAAAAAAAAAAAAAAAAAAA AAA";
560 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
561 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
562 assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAAAAAAA", "AAA"]);
563 assert_eq!(line_truncator, ["AAAAAAAAAAAAAAAAAAAA"]);
564 }
565
566 #[test]
568 fn line_composer_lots_of_spaces() {
569 let width = 20;
570 let text = " ";
571 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
572 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
573 assert_eq!(word_wrapper, [""]);
574 assert_eq!(line_truncator, [" "]);
575 }
576
577 #[test]
580 fn line_composer_char_plus_lots_of_spaces() {
581 let width = 20;
582 let text = "a ";
583 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
584 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
585 assert_eq!(word_wrapper, ["a", ""]);
590 assert_eq!(line_truncator, ["a "]);
591 }
592
593 #[test]
594 fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
595 let width = 20;
596 let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
603 let (word_wrapper, word_wrapper_width, _) =
604 run_composer(Composer::WordWrapper { trim: true }, text, width);
605 assert_eq!(
606 word_wrapper,
607 vec![
608 "コンピュ",
609 "ータ上で文字を扱う場",
610 "合、 典型的には文",
611 "字による 通信を行",
612 "う場合にその両端点で",
613 "は、",
614 ]
615 );
616 assert_eq!(word_wrapper_width, [8, 20, 17, 17, 20, 4]);
618 }
619
620 #[test]
622 fn line_composer_word_wrapper_nbsp() {
623 let width = 20;
624 let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
625 let (word_wrapper, word_wrapper_widths, _) =
626 run_composer(Composer::WordWrapper { trim: true }, text, width);
627 assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA"]);
628 assert_eq!(word_wrapper_widths, [15, 8]);
629
630 let text_space = text.replace('\u{00a0}', " ");
632 let (word_wrapper_space, word_wrapper_widths, _) =
633 run_composer(Composer::WordWrapper { trim: true }, text_space, width);
634 assert_eq!(word_wrapper_space, ["AAAAAAAAAAAAAAA AAAA", "AAA"]);
635 assert_eq!(word_wrapper_widths, [20, 3]);
636 }
637
638 #[test]
639 fn line_composer_word_wrapper_preserve_indentation() {
640 let width = 20;
641 let text = "AAAAAAAAAAAAAAAAAAAA AAA";
642 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
643 assert_eq!(word_wrapper, ["AAAAAAAAAAAAAAAAAAAA", " AAA"]);
644 }
645
646 #[test]
647 fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
648 let width = 10;
649 let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
650 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
651 assert_eq!(
652 word_wrapper,
653 vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
654 );
655 }
656
657 #[test]
658 fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
659 let width = 10;
660 let text = " 4 Indent\n must wrap!";
661 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
662 assert_eq!(
663 word_wrapper,
664 vec![
665 " ",
666 " 4",
667 "Indent",
668 " ",
669 " must",
670 "wrap!"
671 ]
672 );
673 }
674
675 #[test]
676 fn line_composer_zero_width_at_end() {
677 let width = 3;
678 let line = "foo\u{200B}";
679 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
680 let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
681 assert_eq!(word_wrapper, ["foo"]);
682 assert_eq!(line_truncator, ["foo\u{200B}"]);
683 }
684
685 #[test]
686 fn line_composer_preserves_line_alignment() {
687 let width = 20;
688 let lines = vec![
689 Line::from("Something that is left aligned.").alignment(Alignment::Left),
690 Line::from("This is right aligned and half short.").alignment(Alignment::Right),
691 Line::from("This should sit in the center.").alignment(Alignment::Center),
692 ];
693 let (_, _, wrapped_alignments) =
694 run_composer(Composer::WordWrapper { trim: true }, lines.clone(), width);
695 let (_, _, truncated_alignments) = run_composer(Composer::LineTruncator, lines, width);
696 assert_eq!(
697 wrapped_alignments,
698 vec![
699 Alignment::Left,
700 Alignment::Left,
701 Alignment::Right,
702 Alignment::Right,
703 Alignment::Right,
704 Alignment::Center,
705 Alignment::Center
706 ]
707 );
708 assert_eq!(
709 truncated_alignments,
710 vec![Alignment::Left, Alignment::Right, Alignment::Center]
711 );
712 }
713
714 #[test]
715 fn line_composer_zero_width_white_space() {
716 let width = 3;
717 let line = "foo\u{200b}bar";
718 let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
719 assert_eq!(word_wrapper, ["foo", "bar"]);
720 }
721}