1use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4use unicode_segmentation::UnicodeSegmentation;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum TruncateFrom {
9 Start,
11 End,
13 Middle,
15}
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum IndentAdjustment {
20 NoIndent,
22 #[default]
24 SameIndent,
25 ExtraColumns(u32),
27}
28
29pub struct LineWrapper {
31 text_system: Arc<TextSystem>,
32 pub(crate) font_id: FontId,
33 pub(crate) font_size: Pixels,
34 char_widths: CharWidthCache,
35 text_buffer: String,
36}
37
38pub(crate) struct CharWidthCache {
40 ascii: [Option<Pixels>; 128],
41 other: HashMap<char, Pixels>,
42}
43
44impl Default for CharWidthCache {
45 fn default() -> Self {
46 Self {
47 ascii: [None; 128],
48 other: HashMap::default(),
49 }
50 }
51}
52
53impl LineWrapper {
54 pub const MAX_INDENT: u32 = 256;
56
57 pub(crate) fn new(
58 font_id: FontId,
59 font_size: Pixels,
60 text_system: Arc<TextSystem>,
61 char_widths: CharWidthCache,
62 ) -> Self {
63 Self {
64 text_system,
65 font_id,
66 font_size,
67 char_widths,
68 text_buffer: String::new(),
69 }
70 }
71
72 pub(crate) fn into_char_widths(self) -> CharWidthCache {
73 self.char_widths
74 }
75
76 pub fn wrap_line<'a>(
78 &'a mut self,
79 fragments: &'a [LineFragment],
80 wrap_width: Pixels,
81 indent_adjustment: IndentAdjustment,
82 ) -> impl Iterator<Item = Boundary> + 'a {
83 let mut width = px(0.);
84 let mut first_non_whitespace_ix = None;
85 let mut base_indent = None;
86 let mut last_candidate_ix = 0;
87 let mut last_candidate_width = px(0.);
88 let mut last_wrap_ix = 0;
89 let mut index = 0;
90 let Self {
91 text_buffer: text,
92 char_widths,
93 text_system,
94 font_id,
95 font_size,
96 } = self;
97 text.clear();
98 for fragment in fragments {
99 match fragment {
100 LineFragment::Text {
101 text: fragment_text,
102 } => text.push_str(fragment_text),
103 LineFragment::Element { len_utf8, .. } => {
105 text.extend(iter::repeat_n('a', *len_utf8));
106 }
107 }
108 }
109 let mut break_indices = Self::break_indices(text).peekable();
110 let mut grapheme_indices = text
111 .grapheme_indices(true)
112 .map(|(index, _)| index)
113 .peekable();
114 let mut width_for_char = move |character| {
115 width_for_char(char_widths, character, text_system, *font_id, *font_size)
116 };
117 let mut grapheme_index = 0;
118 let mut width_before_grapheme = px(0.);
119 let mut candidates = fragments
120 .iter()
121 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
122 .peekable();
123 iter::from_fn(move || {
124 for candidate in candidates.by_ref() {
125 let ix = index;
126 index += candidate.len_utf8();
127 while grapheme_indices
128 .next_if(|position| *position < ix)
129 .is_some()
130 {}
131 let is_grapheme_boundary = grapheme_indices.next_if_eq(&ix).is_some();
132 if is_grapheme_boundary {
133 grapheme_index = ix;
134 width_before_grapheme = width;
135 }
136 if break_indices.next_if_eq(&ix).is_some()
137 && is_grapheme_boundary
138 && first_non_whitespace_ix.is_some()
139 {
140 last_candidate_ix = ix;
141 last_candidate_width = width;
142 }
143 let item_width = match candidate {
144 WrapBoundaryCandidate::Char { character: c } => {
145 if c == '\n' {
146 continue;
147 }
148
149 if c != ' ' && first_non_whitespace_ix.is_none() {
150 first_non_whitespace_ix = Some(ix);
151 }
152
153 width_for_char(c)
154 }
155 WrapBoundaryCandidate::Element {
156 width: element_width,
157 ..
158 } => {
159 if first_non_whitespace_ix.is_none() {
160 first_non_whitespace_ix = Some(ix);
161 }
162
163 element_width
164 }
165 };
166
167 width += item_width;
168 if width > wrap_width && grapheme_index > last_wrap_ix {
169 let wrap_at_candidate = last_candidate_ix > last_wrap_ix
170 && width - last_candidate_width <= wrap_width;
171
172 let carried_width = if wrap_at_candidate {
173 width - last_candidate_width
174 } else {
175 width - width_before_grapheme
176 };
177
178 if base_indent.is_none() {
184 if let Some(first_non_whitespace_ix) = first_non_whitespace_ix {
185 base_indent =
186 Some(Self::MAX_INDENT.min(
187 first_non_whitespace_ix.saturating_sub(last_wrap_ix) as u32,
188 ));
189 }
190 }
191
192 let next_indent = match indent_adjustment {
193 IndentAdjustment::NoIndent => 0,
194 IndentAdjustment::SameIndent => base_indent.unwrap_or(0),
195 IndentAdjustment::ExtraColumns(extra) => {
196 if let Some(base_indent) = base_indent {
197 let candidate = base_indent.saturating_add(extra);
198 let candidate_indent_width = width_for_char(' ') * candidate as f32;
199 let min_headroom = carried_width.max(width_for_char(' ') * 2.0);
203 if candidate_indent_width + min_headroom > wrap_width {
204 0
205 } else {
206 Self::MAX_INDENT.min(candidate)
207 }
208 } else {
209 0
210 }
211 }
212 };
213
214 if wrap_at_candidate {
215 last_wrap_ix = last_candidate_ix;
216 width -= last_candidate_width;
217 width_before_grapheme -= last_candidate_width;
218 } else {
219 last_wrap_ix = grapheme_index;
221 width -= width_before_grapheme;
222 width_before_grapheme = px(0.);
223 }
224 last_candidate_ix = 0;
225
226 let indent_width = width_for_char(' ') * next_indent as f32;
227 width += indent_width;
228 width_before_grapheme += indent_width;
229
230 return Some(Boundary::new(last_wrap_ix, next_indent));
231 }
232 }
233
234 None
235 })
236 }
237
238 pub fn should_truncate_line(
242 &mut self,
243 line: &str,
244 truncate_width: Pixels,
245 truncation_affix: &str,
246 truncate_from: TruncateFrom,
247 ) -> Option<usize> {
248 let mut width = px(0.);
249 let suffix_width = truncation_affix
250 .chars()
251 .map(|c| self.width_for_char(c))
252 .fold(px(0.0), |a, x| a + x);
253 let mut truncate_ix = 0;
254
255 match truncate_from {
256 TruncateFrom::Start => {
257 for (ix, c) in line.char_indices().rev() {
258 if width + suffix_width < truncate_width {
259 truncate_ix = ix;
260 }
261
262 let char_width = self.width_for_char(c);
263 width += char_width;
264
265 if width.floor() > truncate_width {
266 return Some(truncate_ix);
267 }
268 }
269 }
270 TruncateFrom::End => {
271 for (ix, c) in line.char_indices() {
272 if width + suffix_width < truncate_width {
273 truncate_ix = ix;
274 }
275
276 let char_width = self.width_for_char(c);
277 width += char_width;
278
279 if width.floor() > truncate_width {
280 return Some(truncate_ix);
281 }
282 }
283 }
284 TruncateFrom::Middle => {}
285 }
286
287 None
288 }
289
290 fn should_truncate_line_middle(
291 &mut self,
292 line: &str,
293 truncate_width: Pixels,
294 truncation_affix: &str,
295 ) -> Option<(usize, usize)> {
296 let suffix_width = truncation_affix
297 .chars()
298 .map(|c| self.width_for_char(c))
299 .fold(px(0.0), |a, x| a + x);
300
301 let total_width: Pixels = line
302 .chars()
303 .map(|c| self.width_for_char(c))
304 .fold(px(0.0), |a, x| a + x);
305
306 if total_width <= truncate_width {
307 return None;
308 }
309
310 let content_budget = truncate_width - suffix_width;
311 if content_budget <= px(0.) {
312 return Some((0, line.len()));
313 }
314
315 let front_budget = content_budget * (2.0 / 3.0);
316 let back_budget = content_budget - front_budget;
317
318 let mut front_width = px(0.);
319 let mut front_end_ix = 0usize;
320 for (ix, c) in line.char_indices() {
321 let char_width = self.width_for_char(c);
322 if front_width + char_width > front_budget {
323 break;
324 }
325 front_width += char_width;
326 front_end_ix = ix + c.len_utf8();
327 }
328
329 let mut back_width = px(0.);
330 let mut back_start_ix = line.len();
331 for (ix, c) in line.char_indices().rev() {
332 let char_width = self.width_for_char(c);
333 if back_width + char_width > back_budget {
334 break;
335 }
336 back_width += char_width;
337 back_start_ix = ix;
338 }
339
340 if front_end_ix >= back_start_ix {
341 return Some((0, line.len()));
342 }
343
344 Some((front_end_ix, back_start_ix))
345 }
346
347 pub fn truncate_line<'a>(
349 &mut self,
350 line: SharedString,
351 truncate_width: Pixels,
352 truncation_affix: &str,
353 runs: &'a [TextRun],
354 truncate_from: TruncateFrom,
355 ) -> (SharedString, Cow<'a, [TextRun]>) {
356 if truncate_from == TruncateFrom::Middle {
357 if let Some((front_end_ix, back_start_ix)) =
358 self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
359 {
360 let result = SharedString::from(format!(
361 "{}{truncation_affix}{}",
362 &line[..front_end_ix],
363 &line[back_start_ix..]
364 ));
365 let mut runs = runs.to_vec();
366 update_runs_after_middle_truncation(
367 truncation_affix,
368 &mut runs,
369 front_end_ix,
370 back_start_ix,
371 );
372 return (result, Cow::Owned(runs));
373 } else {
374 return (line, Cow::Borrowed(runs));
375 }
376 }
377
378 if let Some(truncate_ix) =
379 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
380 {
381 let result = match truncate_from {
382 TruncateFrom::Start => SharedString::from(format!(
383 "{truncation_affix}{}",
384 &line[line.ceil_char_boundary(truncate_ix + 1)..]
385 )),
386 TruncateFrom::End => SharedString::from(format!(
387 "{}{truncation_affix}",
388 line[..truncate_ix]
389 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
390 )),
391 TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
392 };
393 let mut runs = runs.to_vec();
394 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
395 (result, Cow::Owned(runs))
396 } else {
397 (line, Cow::Borrowed(runs))
398 }
399 }
400
401 pub fn truncate_wrapped_line<'a>(
411 &mut self,
412 text: SharedString,
413 wrap_width: Pixels,
414 max_lines: usize,
415 truncation_affix: &str,
416 runs: &'a [TextRun],
417 truncate_from: TruncateFrom,
418 ) -> (SharedString, Cow<'a, [TextRun]>) {
419 if max_lines <= 1 || truncate_from == TruncateFrom::Start {
420 return self.truncate_line(
421 text,
422 wrap_width * max_lines,
423 truncation_affix,
424 runs,
425 truncate_from,
426 );
427 }
428 if truncate_from == TruncateFrom::Middle {
429 return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
430 }
431
432 let affix_width: Pixels = truncation_affix
433 .chars()
434 .map(|c| self.width_for_char(c))
435 .sum();
436
437 let mut width = px(0.);
438 let mut line = 0usize;
439 let mut first_non_whitespace_ix = None;
440 let mut last_candidate_ix = 0usize;
441 let mut last_candidate_width = px(0.);
442 let mut last_wrap_ix = 0usize;
443 let mut indent: Option<u32> = None;
444 let mut truncate_ix = 0usize;
445
446 let mut break_indices = Self::break_indices(&text).peekable();
447 for (ix, c) in text.char_indices() {
448 let is_break = break_indices.next_if_eq(&ix).is_some();
451 if c == '\n' {
452 if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
453 let truncated = text[..truncate_ix]
456 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
457 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
458 let mut runs = runs.to_vec();
459 update_runs_after_truncation(
460 &result,
461 truncation_affix,
462 &mut runs,
463 TruncateFrom::End,
464 );
465 return (result, Cow::Owned(runs));
466 }
467
468 line += 1;
470 width = px(0.);
471 first_non_whitespace_ix = None;
472 last_candidate_ix = 0;
473 last_candidate_width = px(0.);
474 last_wrap_ix = ix + 1;
475 indent = None;
476 truncate_ix = ix + 1;
477 continue;
478 }
479
480 let char_width = self.width_for_char(c);
481
482 if is_break && first_non_whitespace_ix.is_some() {
483 last_candidate_ix = ix;
484 last_candidate_width = width;
485 }
486
487 if c != ' ' && first_non_whitespace_ix.is_none() {
488 first_non_whitespace_ix = Some(ix);
489 }
490
491 width += char_width;
492
493 if line < max_lines - 1 {
494 if width > wrap_width && ix > last_wrap_ix {
496 if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
497 indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
498 }
499
500 if last_candidate_ix > last_wrap_ix {
501 last_wrap_ix = last_candidate_ix;
502 width -= last_candidate_width;
503 last_candidate_ix = 0;
504 } else {
505 last_wrap_ix = ix;
506 width = char_width;
507 }
508
509 if let Some(ind) = indent {
510 width += self.width_for_char(' ') * ind as f32;
511 }
512
513 line += 1;
514 truncate_ix = last_wrap_ix;
515 }
516 } else {
517 if width + affix_width <= wrap_width {
520 truncate_ix = ix + c.len_utf8();
521 }
522
523 if width > wrap_width {
524 let truncated = text[..truncate_ix]
525 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
526 let result = SharedString::from(format!("{truncated}{truncation_affix}"));
527 let mut runs = runs.to_vec();
528 update_runs_after_truncation(
529 &result,
530 truncation_affix,
531 &mut runs,
532 TruncateFrom::End,
533 );
534 return (result, Cow::Owned(runs));
535 }
536 }
537 }
538
539 drop(break_indices);
541 (text, Cow::Borrowed(runs))
542 }
543
544 pub(crate) fn break_indices(text: &str) -> impl Iterator<Item = usize> {
546 icu_segmenter::LineSegmenter::new_for_non_complex_scripts(Default::default())
547 .segment_str(text)
548 }
549
550 #[inline(always)]
551 fn width_for_char(&mut self, c: char) -> Pixels {
552 width_for_char(
553 &mut self.char_widths,
554 c,
555 &self.text_system,
556 self.font_id,
557 self.font_size,
558 )
559 }
560}
561
562#[inline(always)]
563fn width_for_char(
564 char_widths: &mut CharWidthCache,
565 c: char,
566 text_system: &TextSystem,
567 font_id: FontId,
568 font_size: Pixels,
569) -> Pixels {
570 if (c as u32) < 128 {
571 if let Some(cached_width) = char_widths.ascii[c as usize] {
572 cached_width
573 } else {
574 let width = text_system.layout_width(font_id, font_size, c);
575 char_widths.ascii[c as usize] = Some(width);
576 width
577 }
578 } else if let Some(cached_width) = char_widths.other.get(&c) {
579 *cached_width
580 } else {
581 let width = text_system.layout_width(font_id, font_size, c);
582 char_widths.other.insert(c, width);
583 width
584 }
585}
586
587fn update_runs_after_truncation(
588 result: &str,
589 ellipsis: &str,
590 runs: &mut Vec<TextRun>,
591 truncate_from: TruncateFrom,
592) {
593 let mut truncate_at = result.len() - ellipsis.len();
594 match truncate_from {
595 TruncateFrom::Start => {
596 for (run_index, run) in runs.iter_mut().enumerate().rev() {
597 if run.len <= truncate_at {
598 truncate_at -= run.len;
599 } else {
600 run.len = truncate_at + ellipsis.len();
601 runs.splice(..run_index, std::iter::empty());
602 break;
603 }
604 }
605 }
606 TruncateFrom::End => {
607 for (run_index, run) in runs.iter_mut().enumerate() {
608 if run.len <= truncate_at {
609 truncate_at -= run.len;
610 } else {
611 run.len = truncate_at + ellipsis.len();
612 runs.truncate(run_index + 1);
613 break;
614 }
615 }
616 }
617 TruncateFrom::Middle => {
618 unreachable!("Middle truncation calls this function with TruncateFrom::End directly")
619 }
620 }
621}
622
623fn update_runs_after_middle_truncation(
624 ellipsis: &str,
625 runs: &mut Vec<TextRun>,
626 front_end_ix: usize,
627 back_start_ix: usize,
628) {
629 let original_runs = std::mem::take(runs);
630 let mut result_runs: Vec<TextRun> = Vec::with_capacity(original_runs.len());
631
632 let mut front_remaining = front_end_ix;
636 let mut front_done = false;
637 for run in &original_runs {
638 if front_done {
639 break;
640 }
641 if run.len <= front_remaining {
642 result_runs.push(run.clone());
643 front_remaining -= run.len;
644 } else {
645 let mut partial = run.clone();
646 partial.len = front_remaining + ellipsis.len();
647 result_runs.push(partial);
648 front_done = true;
649 }
650 }
651 if !front_done {
652 if let Some(last) = result_runs.last_mut() {
655 last.len += ellipsis.len();
656 } else if let Some(first) = original_runs.first() {
657 let mut affix_run = first.clone();
658 affix_run.len = ellipsis.len();
659 result_runs.push(affix_run);
660 }
661 }
662
663 let mut byte_pos = 0usize;
666 for run in &original_runs {
667 let run_end = byte_pos + run.len;
668 if run_end > back_start_ix {
669 if byte_pos < back_start_ix {
670 let mut partial = run.clone();
672 partial.len = run_end - back_start_ix;
673 result_runs.push(partial);
674 } else {
675 result_runs.push(run.clone());
676 }
677 }
678 byte_pos = run_end;
679 }
680
681 *runs = result_runs;
682}
683
684pub enum LineFragment<'a> {
686 Text {
688 text: &'a str,
690 },
691 Element {
693 width: Pixels,
695 len_utf8: usize,
697 },
698}
699
700impl<'a> LineFragment<'a> {
701 pub fn text(text: &'a str) -> Self {
703 LineFragment::Text { text }
704 }
705
706 pub fn element(width: Pixels, len_utf8: usize) -> Self {
708 LineFragment::Element { width, len_utf8 }
709 }
710
711 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
712 let text = match self {
713 LineFragment::Text { text } => text,
714 LineFragment::Element { .. } => "\0",
715 };
716 text.chars().map(move |character| {
717 if let LineFragment::Element { width, len_utf8 } = self {
718 WrapBoundaryCandidate::Element {
719 width: *width,
720 len_utf8: *len_utf8,
721 }
722 } else {
723 WrapBoundaryCandidate::Char { character }
724 }
725 })
726 }
727}
728
729enum WrapBoundaryCandidate {
730 Char { character: char },
731 Element { width: Pixels, len_utf8: usize },
732}
733
734impl WrapBoundaryCandidate {
735 pub fn len_utf8(&self) -> usize {
736 match self {
737 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
738 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
739 }
740 }
741}
742
743#[derive(Copy, Clone, Debug, PartialEq, Eq)]
745pub struct Boundary {
746 pub ix: usize,
748 pub next_indent: u32,
750}
751
752impl Boundary {
753 fn new(ix: usize, next_indent: u32) -> Self {
754 Self { ix, next_indent }
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
762 #[cfg(target_os = "macos")]
763 use crate::{TextRun, WindowTextSystem, WrapBoundary};
764
765 fn build_wrapper() -> LineWrapper {
766 let dispatcher = TestDispatcher::new(0);
767 let cx = TestAppContext::build(dispatcher, None);
768 let id = cx.text_system().resolve_font(&font(".ZedMono"));
769 LineWrapper::new(
770 id,
771 px(16.),
772 cx.text_system().clone(),
773 CharWidthCache::default(),
774 )
775 }
776
777 fn assert_emergency_wrap_preserves_graphemes(wrapper: &mut LineWrapper) {
778 use unicode_segmentation::UnicodeSegmentation;
779
780 let mut failures = Vec::new();
781 for cluster in ["e\u{301}", "か\u{3099}", "👍🏽", "🇯🇵", "👩\u{200d}💻"] {
782 let cluster_width: Pixels = cluster
783 .chars()
784 .map(|character| wrapper.width_for_char(character))
785 .sum();
786 let first_width = cluster
787 .chars()
788 .next()
789 .map(|character| wrapper.width_for_char(character))
790 .expect("nonempty fixture");
791 for text in [cluster.to_owned(), format!("x{cluster}y")] {
792 let grapheme_boundaries: Vec<_> = text
793 .grapheme_indices(true)
794 .map(|(index, _)| index)
795 .chain(std::iter::once(text.len()))
796 .collect();
797 assert!(
798 LineWrapper::break_indices(&text)
799 .all(|index| grapheme_boundaries.contains(&index)),
800 "UAX #14 boundary inside {text:?}"
801 );
802 let fragments = [LineFragment::text(&text)];
803 let full_width: Pixels = text
804 .chars()
805 .map(|character| wrapper.width_for_char(character))
806 .sum();
807 assert!(
808 wrapper
809 .wrap_line(&fragments, full_width + px(1.), IndentAdjustment::NoIndent)
810 .next()
811 .is_none()
812 );
813 for wrap_width in [
814 first_width - px(0.1),
815 first_width,
816 first_width + px(0.1),
817 cluster_width - px(0.1),
818 cluster_width,
819 cluster_width + px(0.1),
820 ] {
821 let boundaries: Vec<_> = wrapper
822 .wrap_line(&fragments, wrap_width, IndentAdjustment::NoIndent)
823 .map(|boundary| boundary.ix)
824 .collect();
825 let split_fragments: Vec<_> = text
826 .split_inclusive(|_: char| true)
827 .map(LineFragment::text)
828 .collect();
829 let split_boundaries: Vec<_> = wrapper
830 .wrap_line(&split_fragments, wrap_width, IndentAdjustment::NoIndent)
831 .map(|boundary| boundary.ix)
832 .collect();
833 assert_eq!(
834 boundaries, split_boundaries,
835 "fragmentation changed {text:?}"
836 );
837 let invalid: Vec<_> = boundaries
838 .iter()
839 .copied()
840 .filter(|index| !grapheme_boundaries.contains(index))
841 .collect();
842 if !invalid.is_empty() {
843 failures.push(format!("text={text:?}, width={wrap_width:?}, cluster_width={cluster_width:?}, breaks={boundaries:?}, inside_grapheme={invalid:?}"));
844 }
845 }
846 }
847 }
848 assert!(failures.is_empty(), "{}", failures.join("\n"));
849 }
850
851 #[test]
852 fn test_emergency_wrap_preserves_graphemes() {
853 assert_emergency_wrap_preserves_graphemes(&mut build_wrapper());
854 }
855
856 #[test]
857 fn test_emergency_wrap_exact_boundaries() {
858 let mut wrapper = build_wrapper();
859 wrapper.char_widths.ascii.fill(Some(px(1.)));
860 wrapper.char_widths.other.insert('\u{301}', px(1.));
861
862 for (text, width, adjustment, expected) in [
863 (
864 "xe\u{301}y",
865 1.,
866 IndentAdjustment::NoIndent,
867 vec![Boundary::new(1, 0), Boundary::new(4, 0)],
868 ),
869 (
870 " xe\u{301}yze\u{301}q",
871 5.,
872 IndentAdjustment::SameIndent,
873 vec![Boundary::new(6, 2), Boundary::new(8, 2)],
874 ),
875 (
876 " xe\u{301}yze\u{301}q",
877 5.,
878 IndentAdjustment::ExtraColumns(1),
879 vec![
880 Boundary::new(6, 3),
881 Boundary::new(8, 3),
882 Boundary::new(11, 3),
883 ],
884 ),
885 ] {
886 let fragments = [LineFragment::text(text)];
887 assert_eq!(
888 wrapper
889 .wrap_line(&fragments, px(width), adjustment)
890 .collect::<Vec<_>>(),
891 expected,
892 "{text:?}, {adjustment:?}",
893 );
894 }
895 }
896
897 #[test]
898 fn test_emergency_wrap_elements_and_graphemes() {
899 let mut wrapper = build_wrapper();
900 wrapper.char_widths.ascii.fill(Some(px(1.)));
901 wrapper.char_widths.other.insert('\u{301}', px(1.));
902
903 for element_length in [1, 3, 32] {
904 let fragments = [
905 LineFragment::text("xe\u{301}"),
906 LineFragment::element(px(4.), element_length),
907 LineFragment::text("e\u{301}yz"),
908 ];
909 assert_eq!(
910 wrapper
911 .wrap_line(&fragments, px(2.), IndentAdjustment::NoIndent)
912 .collect::<Vec<_>>(),
913 vec![
914 Boundary::new(1, 0),
915 Boundary::new(4, 0),
916 Boundary::new(4 + element_length, 0),
917 Boundary::new(7 + element_length, 0),
918 ],
919 "element_length={element_length}",
920 );
921 }
922 }
923
924 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
925 input_run_len
926 .iter()
927 .map(|run_len| TextRun {
928 len: *run_len,
929 font: Font {
930 family: "Dummy".into(),
931 features: FontFeatures::default(),
932 variations: Default::default(),
933 fallbacks: None,
934 weight: FontWeight::default(),
935 style: FontStyle::Normal,
936 },
937 ..Default::default()
938 })
939 .collect()
940 }
941
942 #[test]
943 fn test_wrap_line() {
944 let mut wrapper = build_wrapper();
945
946 assert_eq!(
947 wrapper
948 .wrap_line(
949 &[LineFragment::text("aa bbb cccc ddddd eeee")],
950 px(72.),
951 IndentAdjustment::default()
952 )
953 .collect::<Vec<_>>(),
954 &[
955 Boundary::new(7, 0),
956 Boundary::new(12, 0),
957 Boundary::new(18, 0)
958 ],
959 );
960 assert_eq!(
961 wrapper
962 .wrap_line(
963 &[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")],
964 px(72.0),
965 IndentAdjustment::default()
966 )
967 .collect::<Vec<_>>(),
968 &[
969 Boundary::new(4, 0),
970 Boundary::new(11, 0),
971 Boundary::new(18, 0)
972 ],
973 );
974 assert_eq!(
975 wrapper
976 .wrap_line(
977 &[LineFragment::text(" aaaaaaa")],
978 px(72.),
979 IndentAdjustment::default()
980 )
981 .collect::<Vec<_>>(),
982 &[
983 Boundary::new(7, 5),
984 Boundary::new(9, 5),
985 Boundary::new(11, 5),
986 ]
987 );
988 assert_eq!(
989 wrapper
990 .wrap_line(
991 &[LineFragment::text(" ")],
992 px(72.),
993 IndentAdjustment::default()
994 )
995 .collect::<Vec<_>>(),
996 &[
997 Boundary::new(7, 0),
998 Boundary::new(14, 0),
999 Boundary::new(21, 0)
1000 ]
1001 );
1002 assert_eq!(
1003 wrapper
1004 .wrap_line(
1005 &[LineFragment::text(" aaaaaaaaaaaaaa")],
1006 px(72.),
1007 IndentAdjustment::default()
1008 )
1009 .collect::<Vec<_>>(),
1010 &[
1011 Boundary::new(7, 0),
1012 Boundary::new(14, 3),
1013 Boundary::new(18, 3),
1014 Boundary::new(22, 3),
1015 ]
1016 );
1017
1018 assert_eq!(
1020 wrapper
1021 .wrap_line(
1022 &[
1023 LineFragment::text("aa bbb "),
1024 LineFragment::text("cccc ddddd eeee")
1025 ],
1026 px(72.),
1027 IndentAdjustment::default()
1028 )
1029 .collect::<Vec<_>>(),
1030 &[
1031 Boundary::new(7, 0),
1032 Boundary::new(12, 0),
1033 Boundary::new(18, 0)
1034 ],
1035 );
1036
1037 assert_eq!(
1039 wrapper
1040 .wrap_line(
1041 &[
1042 LineFragment::text("aa "),
1043 LineFragment::element(px(20.), 1),
1044 LineFragment::text(" bbb "),
1045 LineFragment::element(px(30.), 1),
1046 LineFragment::text(" cccc")
1047 ],
1048 px(72.),
1049 IndentAdjustment::default()
1050 )
1051 .collect::<Vec<_>>(),
1052 &[
1053 Boundary::new(5, 0),
1054 Boundary::new(9, 0),
1055 Boundary::new(11, 0)
1056 ],
1057 );
1058
1059 assert_eq!(
1061 wrapper
1062 .wrap_line(
1063 &[
1064 LineFragment::element(px(50.), 1),
1065 LineFragment::text(" aaaa bbbb cccc dddd")
1066 ],
1067 px(72.),
1068 IndentAdjustment::default()
1069 )
1070 .collect::<Vec<_>>(),
1071 &[
1072 Boundary::new(2, 0),
1073 Boundary::new(7, 0),
1074 Boundary::new(12, 0),
1075 Boundary::new(17, 0)
1076 ],
1077 );
1078
1079 assert_eq!(
1081 wrapper
1082 .wrap_line(
1083 &[
1084 LineFragment::text("short text "),
1085 LineFragment::element(px(100.), 1),
1086 LineFragment::text(" more text")
1087 ],
1088 px(72.),
1089 IndentAdjustment::default()
1090 )
1091 .collect::<Vec<_>>(),
1092 &[
1093 Boundary::new(6, 0),
1094 Boundary::new(11, 0),
1095 Boundary::new(12, 0),
1096 Boundary::new(18, 0)
1097 ],
1098 );
1099
1100 assert_eq!(
1102 wrapper
1103 .wrap_line(
1104 &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")],
1105 px(72.0),
1106 IndentAdjustment::default()
1107 )
1108 .collect::<Vec<_>>(),
1109 &[Boundary::new(12, 0),], );
1111 }
1112
1113 #[test]
1114 fn test_wrap_line_break_opportunities() {
1115 let mut wrapper = build_wrapper();
1116 for (text, fitting_prefix, expected_index) in [
1117 ("aaa required!", "aaa required", 4),
1118 ("aaa foo/bar", "aaa foo/", 8),
1119 ("aaa (hello)", "aaa (hello", 4),
1120 ("aaa αβγδε", "aaa αβγδ", 4),
1121 ("aaa a\u{a0}b", "aaa a\u{a0}", 4),
1122 ("aaa 你好!", "aaa 你好", 7),
1123 ] {
1124 let wrap_width = fitting_prefix
1125 .chars()
1126 .map(|character| wrapper.width_for_char(character))
1127 .sum();
1128 assert_eq!(
1129 wrapper
1130 .wrap_line(
1131 &[LineFragment::text(text)],
1132 wrap_width,
1133 IndentAdjustment::default()
1134 )
1135 .collect::<Vec<_>>(),
1136 [Boundary::new(expected_index, 0)],
1137 "{text:?}",
1138 );
1139 }
1140 }
1141
1142 #[test]
1143 fn test_truncate_line_end() {
1144 let mut wrapper = build_wrapper();
1145
1146 fn perform_test(
1147 wrapper: &mut LineWrapper,
1148 text: &'static str,
1149 expected: &'static str,
1150 ellipsis: &str,
1151 ) {
1152 let dummy_run_lens = vec![text.len()];
1153 let dummy_runs = generate_test_runs(&dummy_run_lens);
1154 let (result, dummy_runs) = wrapper.truncate_line(
1155 text.into(),
1156 px(220.),
1157 ellipsis,
1158 &dummy_runs,
1159 TruncateFrom::End,
1160 );
1161 assert_eq!(result, expected);
1162 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1163 }
1164
1165 perform_test(
1166 &mut wrapper,
1167 "aa bbb cccc ddddd eeee ffff gggg",
1168 "aa bbb cccc ddddd eeee",
1169 "",
1170 );
1171 perform_test(
1172 &mut wrapper,
1173 "aa bbb cccc ddddd eeee ffff gggg",
1174 "aa bbb cccc ddddd eee…",
1175 "…",
1176 );
1177 perform_test(
1178 &mut wrapper,
1179 "aa bbb cccc ddddd eeee ffff gggg",
1180 "aa bbb cccc dddd......",
1181 "......",
1182 );
1183 perform_test(
1184 &mut wrapper,
1185 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
1186 "aa bbb cccc 🦀🦀🦀🦀…",
1187 "…",
1188 );
1189 }
1190
1191 #[test]
1192 fn test_truncate_line_start() {
1193 let mut wrapper = build_wrapper();
1194
1195 #[track_caller]
1196 fn perform_test(
1197 wrapper: &mut LineWrapper,
1198 text: &'static str,
1199 expected: &'static str,
1200 ellipsis: &str,
1201 ) {
1202 let dummy_run_lens = vec![text.len()];
1203 let dummy_runs = generate_test_runs(&dummy_run_lens);
1204 let (result, dummy_runs) = wrapper.truncate_line(
1205 text.into(),
1206 px(220.),
1207 ellipsis,
1208 &dummy_runs,
1209 TruncateFrom::Start,
1210 );
1211 assert_eq!(result, expected);
1212 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1213 }
1214
1215 perform_test(
1216 &mut wrapper,
1217 "aaaa bbbb cccc ddddd eeee fff gg",
1218 "cccc ddddd eeee fff gg",
1219 "",
1220 );
1221 perform_test(
1222 &mut wrapper,
1223 "aaaa bbbb cccc ddddd eeee fff gg",
1224 "…ccc ddddd eeee fff gg",
1225 "…",
1226 );
1227 perform_test(
1228 &mut wrapper,
1229 "aaaa bbbb cccc ddddd eeee fff gg",
1230 "......dddd eeee fff gg",
1231 "......",
1232 );
1233 perform_test(
1234 &mut wrapper,
1235 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
1236 "…🦀🦀🦀🦀 eeee fff gg",
1237 "…",
1238 );
1239 }
1240
1241 #[test]
1242 fn test_truncate_multiple_runs_end() {
1243 let mut wrapper = build_wrapper();
1244
1245 fn perform_test(
1246 wrapper: &mut LineWrapper,
1247 text: &'static str,
1248 expected: &str,
1249 run_lens: &[usize],
1250 result_run_len: &[usize],
1251 line_width: Pixels,
1252 ) {
1253 let dummy_runs = generate_test_runs(run_lens);
1254 let (result, dummy_runs) =
1255 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
1256 assert_eq!(result, expected);
1257 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1258 assert_eq!(run.len, *result_len);
1259 }
1260 }
1261 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1268 perform_test(
1276 &mut wrapper,
1277 "abcdefghijkl",
1278 "abcdef…",
1279 &[4, 4, 4],
1280 &[4, 5],
1281 px(70.),
1282 );
1283 perform_test(
1291 &mut wrapper,
1292 "abcdefghijkl",
1293 "abcdefgh…",
1294 &[4, 4, 4],
1295 &[4, 4, 3],
1296 px(90.),
1297 );
1298 }
1299
1300 #[test]
1301 fn test_truncate_multiple_runs_start() {
1302 let mut wrapper = build_wrapper();
1303
1304 #[track_caller]
1305 fn perform_test(
1306 wrapper: &mut LineWrapper,
1307 text: &'static str,
1308 expected: &str,
1309 run_lens: &[usize],
1310 result_run_len: &[usize],
1311 line_width: Pixels,
1312 ) {
1313 let dummy_runs = generate_test_runs(run_lens);
1314 let (result, dummy_runs) = wrapper.truncate_line(
1315 text.into(),
1316 line_width,
1317 "…",
1318 &dummy_runs,
1319 TruncateFrom::Start,
1320 );
1321 assert_eq!(result, expected);
1322 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1323 assert_eq!(run.len, *result_len);
1324 }
1325 }
1326 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1333 perform_test(
1341 &mut wrapper,
1342 "abcdefghijkl",
1343 "…ghijkl",
1344 &[4, 4, 4],
1345 &[5, 4],
1346 px(70.),
1347 );
1348 perform_test(
1356 &mut wrapper,
1357 "abcdefghijkl",
1358 "…efghijkl",
1359 &[4, 4, 4],
1360 &[3, 4, 4],
1361 px(90.),
1362 );
1363 }
1364
1365 #[test]
1366 fn test_update_run_after_truncation_end() {
1367 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
1368 let mut dummy_runs = generate_test_runs(run_lens);
1369 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
1370 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
1371 assert_eq!(run.len, *result_len);
1372 }
1373 }
1374 perform_test("abcd…", &[12], &[7]);
1381 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1389 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1397 }
1398
1399 #[cfg(target_os = "macos")]
1401 use crate as gpui;
1402
1403 #[cfg(target_os = "macos")]
1405 #[crate::test]
1406 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
1407 cx.update(|cx| {
1408 let text_system = WindowTextSystem::new(cx.text_system().clone());
1409
1410 let normal = TextRun {
1411 len: 0,
1412 font: font("Helvetica"),
1413 color: Default::default(),
1414 underline: Default::default(),
1415 ..Default::default()
1416 };
1417 let bold = TextRun {
1418 len: 0,
1419 font: font("Helvetica").bold(),
1420 ..Default::default()
1421 };
1422
1423 let text = "aa bbb cccc ddddd eeee".into();
1424 let lines = text_system
1425 .shape_text(
1426 text,
1427 px(16.),
1428 &[
1429 normal.with_len(4),
1430 bold.with_len(5),
1431 normal.with_len(6),
1432 bold.with_len(1),
1433 normal.with_len(7),
1434 ],
1435 Some(px(72.)),
1436 None,
1437 )
1438 .unwrap();
1439
1440 assert_eq!(
1441 lines[0].layout.wrap_boundaries(),
1442 &[
1443 WrapBoundary {
1444 run_ix: 0,
1445 glyph_ix: 7
1446 },
1447 WrapBoundary {
1448 run_ix: 0,
1449 glyph_ix: 12
1450 },
1451 WrapBoundary {
1452 run_ix: 0,
1453 glyph_ix: 18
1454 }
1455 ],
1456 );
1457 });
1458 }
1459
1460 #[test]
1461 fn test_multiline_truncation_fits_within_wrapped_lines() {
1462 let mut wrapper = build_wrapper();
1463
1464 let text: &str = "aa bbbbbb cccccc dddddd eeee ffff";
1476 let wrap_width = px(72.);
1477 let max_lines: usize = 2;
1478
1479 let runs = generate_test_runs(&[text.len()]);
1480 let (truncated, _) = wrapper.truncate_wrapped_line(
1481 text.into(),
1482 wrap_width,
1483 max_lines,
1484 "\u{2026}",
1485 &runs,
1486 TruncateFrom::End,
1487 );
1488
1489 let wrap_count = wrapper
1491 .wrap_line(
1492 &[LineFragment::text(&truncated)],
1493 wrap_width,
1494 IndentAdjustment::default(),
1495 )
1496 .count();
1497
1498 assert!(
1499 wrap_count < max_lines,
1500 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1501 truncated,
1502 wrap_count + 1,
1503 max_lines
1504 );
1505
1506 assert!(
1508 truncated.ends_with('\u{2026}'),
1509 "Truncated text '{}' should end with ellipsis",
1510 truncated
1511 );
1512 }
1513
1514 #[test]
1515 fn test_multiline_truncation_no_truncation_needed() {
1516 let mut wrapper = build_wrapper();
1517
1518 let text: &str = "aa bbb cccccc";
1521 let wrap_width = px(72.);
1522 let max_lines: usize = 2;
1523
1524 let runs = generate_test_runs(&[text.len()]);
1525 let (result, _) = wrapper.truncate_wrapped_line(
1526 text.into(),
1527 wrap_width,
1528 max_lines,
1529 "\u{2026}",
1530 &runs,
1531 TruncateFrom::End,
1532 );
1533
1534 assert_eq!(
1535 result.as_ref(),
1536 text,
1537 "Text that fits should not be modified"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_multiline_truncation_three_lines() {
1543 let mut wrapper = build_wrapper();
1544
1545 let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj";
1546 let wrap_width = px(72.);
1547 let max_lines: usize = 3;
1548
1549 let runs = generate_test_runs(&[text.len()]);
1550 let (truncated, _) = wrapper.truncate_wrapped_line(
1551 text.into(),
1552 wrap_width,
1553 max_lines,
1554 "\u{2026}",
1555 &runs,
1556 TruncateFrom::End,
1557 );
1558
1559 let wrap_count = wrapper
1560 .wrap_line(
1561 &[LineFragment::text(&truncated)],
1562 wrap_width,
1563 IndentAdjustment::default(),
1564 )
1565 .count();
1566
1567 assert!(
1568 wrap_count < max_lines,
1569 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1570 truncated,
1571 wrap_count + 1,
1572 max_lines
1573 );
1574
1575 assert!(
1576 truncated.ends_with('\u{2026}'),
1577 "Truncated text '{}' should end with ellipsis",
1578 truncated
1579 );
1580 }
1581
1582 #[test]
1583 fn test_multiline_truncation_preserves_breaks_after_empty_lines() {
1584 let mut wrapper = build_wrapper();
1585 for prefix in ["", "\n", "\n\n", "hello\n\n"] {
1586 let text = format!("{prefix}aaa required! more words follow");
1587 let runs = generate_test_runs(&[text.len()]);
1588 let max_lines = prefix.bytes().filter(|byte| *byte == b'\n').count() + 2;
1589 let (truncated, _) = wrapper.truncate_wrapped_line(
1590 text.into(),
1591 px(72.),
1592 max_lines,
1593 "…",
1594 &runs,
1595 TruncateFrom::End,
1596 );
1597 assert_eq!(
1598 truncated.as_ref(),
1599 format!("{prefix}aaa {}…", &"required!"[..6]),
1600 "prefix: {prefix:?}",
1601 );
1602 }
1603 }
1604
1605 #[test]
1606 fn test_multiline_truncation_with_newlines() {
1607 let mut wrapper = build_wrapper();
1608
1609 let text: &str = "hello\nworld foo bar baz";
1614 let wrap_width = px(72.);
1615 let max_lines: usize = 2;
1616
1617 let runs = generate_test_runs(&[text.len()]);
1618 let (truncated, _) = wrapper.truncate_wrapped_line(
1619 text.into(),
1620 wrap_width,
1621 max_lines,
1622 "\u{2026}",
1623 &runs,
1624 TruncateFrom::End,
1625 );
1626
1627 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1629 assert_eq!(
1630 parts.len(),
1631 2,
1632 "Newline should be preserved: '{}'",
1633 truncated
1634 );
1635 assert_eq!(parts[0], "hello");
1636
1637 let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum();
1639 assert!(
1640 second_line_width <= wrap_width,
1641 "Second line '{}' ({}px) exceeds wrap_width ({}px)",
1642 parts[1],
1643 second_line_width,
1644 wrap_width
1645 );
1646 assert!(
1647 truncated.ends_with('\u{2026}'),
1648 "Should end with ellipsis: '{}'",
1649 truncated
1650 );
1651 }
1652
1653 #[test]
1654 fn test_multiline_truncation_newline_on_last_line() {
1655 let mut wrapper = build_wrapper();
1656
1657 let text: &str = "hello\nworld\nmore";
1661 let wrap_width = px(72.);
1662 let max_lines: usize = 2;
1663
1664 let runs = generate_test_runs(&[text.len()]);
1665 let (truncated, _) = wrapper.truncate_wrapped_line(
1666 text.into(),
1667 wrap_width,
1668 max_lines,
1669 "\u{2026}",
1670 &runs,
1671 TruncateFrom::End,
1672 );
1673
1674 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1675 assert_eq!(parts[0], "hello");
1676 assert!(
1677 truncated.ends_with('\u{2026}'),
1678 "Should end with ellipsis since there's more content: '{}'",
1679 truncated
1680 );
1681 }
1682
1683 #[test]
1684 fn test_truncate_line_middle() {
1685 let mut wrapper = build_wrapper();
1686
1687 let short_text = "hello world";
1689 let runs = generate_test_runs(&[short_text.len()]);
1690 let (result, result_runs) = wrapper.truncate_line(
1691 short_text.into(),
1692 px(10000.),
1693 "…",
1694 &runs,
1695 TruncateFrom::Middle,
1696 );
1697 assert_eq!(result.as_ref(), short_text);
1698 assert_eq!(result_runs.len(), 1);
1699 assert_eq!(result_runs[0].len, short_text.len());
1700
1701 let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1703 let runs = generate_test_runs(&[long_text.len()]);
1704 let (result, _result_runs) =
1705 wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle);
1706 assert!(
1707 result.contains('…'),
1708 "Middle-truncated result should contain '…', got: '{}'",
1709 result
1710 );
1711 assert!(
1712 result.chars().count() < long_text.chars().count(),
1713 "Middle-truncated result should be shorter than original"
1714 );
1715 assert_eq!(
1716 result.chars().next(),
1717 long_text.chars().next(),
1718 "Result should start with the same first character as original"
1719 );
1720 assert_eq!(
1721 result.chars().last(),
1722 long_text.chars().last(),
1723 "Result should end with the same last character as original"
1724 );
1725
1726 let text = "abcdef";
1729 let runs = generate_test_runs(&[text.len()]);
1730 let (result, result_runs) =
1731 wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle);
1732 assert_eq!(result.as_ref(), "…");
1733 assert_eq!(result_runs.len(), 1);
1734 assert_eq!(result_runs[0].len, "…".len());
1735
1736 let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1739 let run_lens = [20, 20, multi_run_text.len() - 40];
1740 let runs = generate_test_runs(&run_lens);
1741 let (result, result_runs) = wrapper.truncate_line(
1742 multi_run_text.into(),
1743 px(100.),
1744 "…",
1745 &runs,
1746 TruncateFrom::Middle,
1747 );
1748 let total_run_len: usize = result_runs.iter().map(|r| r.len).sum();
1749 assert_eq!(
1750 total_run_len,
1751 result.len(),
1752 "Sum of run lengths ({}) should equal result byte length ({})",
1753 total_run_len,
1754 result.len()
1755 );
1756 }
1757
1758 #[test]
1759 fn test_multiline_truncation_trailing_newline() {
1760 let mut wrapper = build_wrapper();
1761
1762 let text: &str = "hello\nworld\n";
1765 let wrap_width = px(72.);
1766 let max_lines: usize = 2;
1767
1768 let runs = generate_test_runs(&[text.len()]);
1769 let (result, _) = wrapper.truncate_wrapped_line(
1770 text.into(),
1771 wrap_width,
1772 max_lines,
1773 "\u{2026}",
1774 &runs,
1775 TruncateFrom::End,
1776 );
1777
1778 assert!(
1779 !result.ends_with('\u{2026}'),
1780 "Trailing newline with no content should not add ellipsis: '{}'",
1781 result
1782 );
1783 }
1784
1785 #[test]
1786 fn test_multiline_truncation_newline_fits_exactly() {
1787 let mut wrapper = build_wrapper();
1788
1789 let text: &str = "hello\nworld";
1792 let wrap_width = px(72.);
1793 let max_lines: usize = 2;
1794
1795 let runs = generate_test_runs(&[text.len()]);
1796 let (result, _) = wrapper.truncate_wrapped_line(
1797 text.into(),
1798 wrap_width,
1799 max_lines,
1800 "\u{2026}",
1801 &runs,
1802 TruncateFrom::End,
1803 );
1804
1805 assert_eq!(
1806 result.as_ref(),
1807 text,
1808 "Text that fits exactly should not be modified: '{}'",
1809 result
1810 );
1811 }
1812
1813 #[test]
1814 fn test_extra_columns_overflow_guard() {
1815 let mut wrapper = build_wrapper();
1816 let space_width = wrapper.width_for_char(' ');
1817
1818 let text = " ab cd ef gh";
1820 let wrap_width = space_width * 10.0;
1821
1822 assert_eq!(
1829 wrapper
1830 .wrap_line(
1831 &[LineFragment::text(text)],
1832 wrap_width,
1833 IndentAdjustment::ExtraColumns(8),
1834 )
1835 .collect::<Vec<_>>(),
1836 &[Boundary::new(9, 0)]
1837 );
1838
1839 assert_eq!(
1852 wrapper
1853 .wrap_line(
1854 &[LineFragment::text(text)],
1855 wrap_width,
1856 IndentAdjustment::ExtraColumns(2),
1857 )
1858 .collect::<Vec<_>>(),
1859 &[
1860 Boundary::new(9, 8),
1861 Boundary::new(11, 8),
1862 Boundary::new(12, 8),
1863 Boundary::new(14, 8),
1864 Boundary::new(15, 8),
1865 ]
1866 );
1867
1868 let full_width_text = " 🦀🦀🦀🦀🦀🦀🦀🦀";
1881 assert_eq!(
1882 wrapper
1883 .wrap_line(
1884 &[LineFragment::text(full_width_text)],
1885 wrap_width,
1886 IndentAdjustment::ExtraColumns(8),
1887 )
1888 .collect::<Vec<_>>(),
1889 &[Boundary::new(17, 0)]
1890 );
1891
1892 assert_eq!(
1899 wrapper
1900 .wrap_line(
1901 &[LineFragment::text("a abcdefghi")],
1902 wrap_width,
1903 IndentAdjustment::ExtraColumns(1),
1904 )
1905 .collect::<Vec<_>>(),
1906 &[Boundary::new(2, 1)]
1907 );
1908
1909 assert_eq!(
1916 wrapper
1917 .wrap_line(
1918 &[LineFragment::text("a abcdefghi")],
1919 wrap_width,
1920 IndentAdjustment::ExtraColumns(2),
1921 )
1922 .collect::<Vec<_>>(),
1923 &[Boundary::new(2, 0)]
1924 );
1925
1926 let carried_text = "a abcdefghij";
1938 assert_eq!(
1939 wrapper
1940 .wrap_line(
1941 &[LineFragment::text(carried_text)],
1942 wrap_width,
1943 IndentAdjustment::ExtraColumns(8),
1944 )
1945 .collect::<Vec<_>>(),
1946 &[Boundary::new(2, 0)]
1947 );
1948
1949 let element_fragments = [
1967 LineFragment::text("abcdefghijk "),
1968 LineFragment::element(space_width * 8.0, 1),
1969 LineFragment::text("z 12"),
1970 ];
1971 assert_eq!(
1972 wrapper
1973 .wrap_line(
1974 &element_fragments,
1975 wrap_width,
1976 IndentAdjustment::ExtraColumns(8),
1977 )
1978 .collect::<Vec<_>>(),
1979 &[
1980 Boundary::new(10, 8),
1981 Boundary::new(12, 0),
1982 Boundary::new(15, 8),
1983 ]
1984 );
1985
1986 let spaces = " "; assert_eq!(
1990 wrapper
1991 .wrap_line(
1992 &[LineFragment::text(spaces)],
1993 wrap_width,
1994 IndentAdjustment::ExtraColumns(8),
1995 )
1996 .collect::<Vec<_>>(),
1997 &[Boundary::new(10, 0)]
1998 );
1999
2000 let multiline_indent_text = " ab cd ef gh";
2009 assert_eq!(
2010 wrapper
2011 .wrap_line(
2012 &[LineFragment::text(multiline_indent_text)],
2013 wrap_width,
2014 IndentAdjustment::ExtraColumns(2),
2015 )
2016 .collect::<Vec<_>>(),
2017 &[
2018 Boundary::new(10, 0),
2019 Boundary::new(20, 6),
2020 Boundary::new(23, 6),
2021 ]
2022 );
2023
2024 let oversized_suffix_fragments = [
2041 LineFragment::text("aaaaaaaaaaaaaaaaaaa a"),
2042 LineFragment::element(space_width * 10.0, 1),
2043 ];
2044 assert_eq!(
2045 wrapper
2046 .wrap_line(
2047 &oversized_suffix_fragments,
2048 wrap_width,
2049 IndentAdjustment::ExtraColumns(2),
2050 )
2051 .collect::<Vec<_>>(),
2052 &[
2053 Boundary::new(10, 2),
2054 Boundary::new(18, 2),
2055 Boundary::new(21, 0),
2056 ]
2057 );
2058
2059 assert_eq!(
2060 wrapper
2061 .wrap_line(
2062 &[LineFragment::text(" aaaaaaaa")],
2063 space_width * 4.0,
2064 IndentAdjustment::ExtraColumns(u32::MAX),
2065 )
2066 .collect::<Vec<_>>(),
2067 &[Boundary::new(4, 0), Boundary::new(8, 0)]
2068 );
2069 }
2070}