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 fallbacks: None,
933 weight: FontWeight::default(),
934 style: FontStyle::Normal,
935 },
936 ..Default::default()
937 })
938 .collect()
939 }
940
941 #[test]
942 fn test_wrap_line() {
943 let mut wrapper = build_wrapper();
944
945 assert_eq!(
946 wrapper
947 .wrap_line(
948 &[LineFragment::text("aa bbb cccc ddddd eeee")],
949 px(72.),
950 IndentAdjustment::default()
951 )
952 .collect::<Vec<_>>(),
953 &[
954 Boundary::new(7, 0),
955 Boundary::new(12, 0),
956 Boundary::new(18, 0)
957 ],
958 );
959 assert_eq!(
960 wrapper
961 .wrap_line(
962 &[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")],
963 px(72.0),
964 IndentAdjustment::default()
965 )
966 .collect::<Vec<_>>(),
967 &[
968 Boundary::new(4, 0),
969 Boundary::new(11, 0),
970 Boundary::new(18, 0)
971 ],
972 );
973 assert_eq!(
974 wrapper
975 .wrap_line(
976 &[LineFragment::text(" aaaaaaa")],
977 px(72.),
978 IndentAdjustment::default()
979 )
980 .collect::<Vec<_>>(),
981 &[
982 Boundary::new(7, 5),
983 Boundary::new(9, 5),
984 Boundary::new(11, 5),
985 ]
986 );
987 assert_eq!(
988 wrapper
989 .wrap_line(
990 &[LineFragment::text(" ")],
991 px(72.),
992 IndentAdjustment::default()
993 )
994 .collect::<Vec<_>>(),
995 &[
996 Boundary::new(7, 0),
997 Boundary::new(14, 0),
998 Boundary::new(21, 0)
999 ]
1000 );
1001 assert_eq!(
1002 wrapper
1003 .wrap_line(
1004 &[LineFragment::text(" aaaaaaaaaaaaaa")],
1005 px(72.),
1006 IndentAdjustment::default()
1007 )
1008 .collect::<Vec<_>>(),
1009 &[
1010 Boundary::new(7, 0),
1011 Boundary::new(14, 3),
1012 Boundary::new(18, 3),
1013 Boundary::new(22, 3),
1014 ]
1015 );
1016
1017 assert_eq!(
1019 wrapper
1020 .wrap_line(
1021 &[
1022 LineFragment::text("aa bbb "),
1023 LineFragment::text("cccc ddddd eeee")
1024 ],
1025 px(72.),
1026 IndentAdjustment::default()
1027 )
1028 .collect::<Vec<_>>(),
1029 &[
1030 Boundary::new(7, 0),
1031 Boundary::new(12, 0),
1032 Boundary::new(18, 0)
1033 ],
1034 );
1035
1036 assert_eq!(
1038 wrapper
1039 .wrap_line(
1040 &[
1041 LineFragment::text("aa "),
1042 LineFragment::element(px(20.), 1),
1043 LineFragment::text(" bbb "),
1044 LineFragment::element(px(30.), 1),
1045 LineFragment::text(" cccc")
1046 ],
1047 px(72.),
1048 IndentAdjustment::default()
1049 )
1050 .collect::<Vec<_>>(),
1051 &[
1052 Boundary::new(5, 0),
1053 Boundary::new(9, 0),
1054 Boundary::new(11, 0)
1055 ],
1056 );
1057
1058 assert_eq!(
1060 wrapper
1061 .wrap_line(
1062 &[
1063 LineFragment::element(px(50.), 1),
1064 LineFragment::text(" aaaa bbbb cccc dddd")
1065 ],
1066 px(72.),
1067 IndentAdjustment::default()
1068 )
1069 .collect::<Vec<_>>(),
1070 &[
1071 Boundary::new(2, 0),
1072 Boundary::new(7, 0),
1073 Boundary::new(12, 0),
1074 Boundary::new(17, 0)
1075 ],
1076 );
1077
1078 assert_eq!(
1080 wrapper
1081 .wrap_line(
1082 &[
1083 LineFragment::text("short text "),
1084 LineFragment::element(px(100.), 1),
1085 LineFragment::text(" more text")
1086 ],
1087 px(72.),
1088 IndentAdjustment::default()
1089 )
1090 .collect::<Vec<_>>(),
1091 &[
1092 Boundary::new(6, 0),
1093 Boundary::new(11, 0),
1094 Boundary::new(12, 0),
1095 Boundary::new(18, 0)
1096 ],
1097 );
1098
1099 assert_eq!(
1101 wrapper
1102 .wrap_line(
1103 &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")],
1104 px(72.0),
1105 IndentAdjustment::default()
1106 )
1107 .collect::<Vec<_>>(),
1108 &[Boundary::new(12, 0),], );
1110 }
1111
1112 #[test]
1113 fn test_wrap_line_break_opportunities() {
1114 let mut wrapper = build_wrapper();
1115 for (text, fitting_prefix, expected_index) in [
1116 ("aaa required!", "aaa required", 4),
1117 ("aaa foo/bar", "aaa foo/", 8),
1118 ("aaa (hello)", "aaa (hello", 4),
1119 ("aaa αβγδε", "aaa αβγδ", 4),
1120 ("aaa a\u{a0}b", "aaa a\u{a0}", 4),
1121 ("aaa 你好!", "aaa 你好", 7),
1122 ] {
1123 let wrap_width = fitting_prefix
1124 .chars()
1125 .map(|character| wrapper.width_for_char(character))
1126 .sum();
1127 assert_eq!(
1128 wrapper
1129 .wrap_line(
1130 &[LineFragment::text(text)],
1131 wrap_width,
1132 IndentAdjustment::default()
1133 )
1134 .collect::<Vec<_>>(),
1135 [Boundary::new(expected_index, 0)],
1136 "{text:?}",
1137 );
1138 }
1139 }
1140
1141 #[test]
1142 fn test_truncate_line_end() {
1143 let mut wrapper = build_wrapper();
1144
1145 fn perform_test(
1146 wrapper: &mut LineWrapper,
1147 text: &'static str,
1148 expected: &'static str,
1149 ellipsis: &str,
1150 ) {
1151 let dummy_run_lens = vec![text.len()];
1152 let dummy_runs = generate_test_runs(&dummy_run_lens);
1153 let (result, dummy_runs) = wrapper.truncate_line(
1154 text.into(),
1155 px(220.),
1156 ellipsis,
1157 &dummy_runs,
1158 TruncateFrom::End,
1159 );
1160 assert_eq!(result, expected);
1161 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1162 }
1163
1164 perform_test(
1165 &mut wrapper,
1166 "aa bbb cccc ddddd eeee ffff gggg",
1167 "aa bbb cccc ddddd eeee",
1168 "",
1169 );
1170 perform_test(
1171 &mut wrapper,
1172 "aa bbb cccc ddddd eeee ffff gggg",
1173 "aa bbb cccc ddddd eee…",
1174 "…",
1175 );
1176 perform_test(
1177 &mut wrapper,
1178 "aa bbb cccc ddddd eeee ffff gggg",
1179 "aa bbb cccc dddd......",
1180 "......",
1181 );
1182 perform_test(
1183 &mut wrapper,
1184 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
1185 "aa bbb cccc 🦀🦀🦀🦀…",
1186 "…",
1187 );
1188 }
1189
1190 #[test]
1191 fn test_truncate_line_start() {
1192 let mut wrapper = build_wrapper();
1193
1194 #[track_caller]
1195 fn perform_test(
1196 wrapper: &mut LineWrapper,
1197 text: &'static str,
1198 expected: &'static str,
1199 ellipsis: &str,
1200 ) {
1201 let dummy_run_lens = vec![text.len()];
1202 let dummy_runs = generate_test_runs(&dummy_run_lens);
1203 let (result, dummy_runs) = wrapper.truncate_line(
1204 text.into(),
1205 px(220.),
1206 ellipsis,
1207 &dummy_runs,
1208 TruncateFrom::Start,
1209 );
1210 assert_eq!(result, expected);
1211 assert_eq!(dummy_runs.first().unwrap().len, result.len());
1212 }
1213
1214 perform_test(
1215 &mut wrapper,
1216 "aaaa bbbb cccc ddddd eeee fff gg",
1217 "cccc ddddd eeee fff gg",
1218 "",
1219 );
1220 perform_test(
1221 &mut wrapper,
1222 "aaaa bbbb cccc ddddd eeee fff gg",
1223 "…ccc ddddd eeee fff gg",
1224 "…",
1225 );
1226 perform_test(
1227 &mut wrapper,
1228 "aaaa bbbb cccc ddddd eeee fff gg",
1229 "......dddd eeee fff gg",
1230 "......",
1231 );
1232 perform_test(
1233 &mut wrapper,
1234 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
1235 "…🦀🦀🦀🦀 eeee fff gg",
1236 "…",
1237 );
1238 }
1239
1240 #[test]
1241 fn test_truncate_multiple_runs_end() {
1242 let mut wrapper = build_wrapper();
1243
1244 fn perform_test(
1245 wrapper: &mut LineWrapper,
1246 text: &'static str,
1247 expected: &str,
1248 run_lens: &[usize],
1249 result_run_len: &[usize],
1250 line_width: Pixels,
1251 ) {
1252 let dummy_runs = generate_test_runs(run_lens);
1253 let (result, dummy_runs) =
1254 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
1255 assert_eq!(result, expected);
1256 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1257 assert_eq!(run.len, *result_len);
1258 }
1259 }
1260 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1267 perform_test(
1275 &mut wrapper,
1276 "abcdefghijkl",
1277 "abcdef…",
1278 &[4, 4, 4],
1279 &[4, 5],
1280 px(70.),
1281 );
1282 perform_test(
1290 &mut wrapper,
1291 "abcdefghijkl",
1292 "abcdefgh…",
1293 &[4, 4, 4],
1294 &[4, 4, 3],
1295 px(90.),
1296 );
1297 }
1298
1299 #[test]
1300 fn test_truncate_multiple_runs_start() {
1301 let mut wrapper = build_wrapper();
1302
1303 #[track_caller]
1304 fn perform_test(
1305 wrapper: &mut LineWrapper,
1306 text: &'static str,
1307 expected: &str,
1308 run_lens: &[usize],
1309 result_run_len: &[usize],
1310 line_width: Pixels,
1311 ) {
1312 let dummy_runs = generate_test_runs(run_lens);
1313 let (result, dummy_runs) = wrapper.truncate_line(
1314 text.into(),
1315 line_width,
1316 "…",
1317 &dummy_runs,
1318 TruncateFrom::Start,
1319 );
1320 assert_eq!(result, expected);
1321 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
1322 assert_eq!(run.len, *result_len);
1323 }
1324 }
1325 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1332 perform_test(
1340 &mut wrapper,
1341 "abcdefghijkl",
1342 "…ghijkl",
1343 &[4, 4, 4],
1344 &[5, 4],
1345 px(70.),
1346 );
1347 perform_test(
1355 &mut wrapper,
1356 "abcdefghijkl",
1357 "…efghijkl",
1358 &[4, 4, 4],
1359 &[3, 4, 4],
1360 px(90.),
1361 );
1362 }
1363
1364 #[test]
1365 fn test_update_run_after_truncation_end() {
1366 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
1367 let mut dummy_runs = generate_test_runs(run_lens);
1368 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
1369 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
1370 assert_eq!(run.len, *result_len);
1371 }
1372 }
1373 perform_test("abcd…", &[12], &[7]);
1380 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1388 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1396 }
1397
1398 #[cfg(target_os = "macos")]
1400 use crate as gpui;
1401
1402 #[cfg(target_os = "macos")]
1404 #[crate::test]
1405 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
1406 cx.update(|cx| {
1407 let text_system = WindowTextSystem::new(cx.text_system().clone());
1408
1409 let normal = TextRun {
1410 len: 0,
1411 font: font("Helvetica"),
1412 color: Default::default(),
1413 underline: Default::default(),
1414 ..Default::default()
1415 };
1416 let bold = TextRun {
1417 len: 0,
1418 font: font("Helvetica").bold(),
1419 ..Default::default()
1420 };
1421
1422 let text = "aa bbb cccc ddddd eeee".into();
1423 let lines = text_system
1424 .shape_text(
1425 text,
1426 px(16.),
1427 &[
1428 normal.with_len(4),
1429 bold.with_len(5),
1430 normal.with_len(6),
1431 bold.with_len(1),
1432 normal.with_len(7),
1433 ],
1434 Some(px(72.)),
1435 None,
1436 )
1437 .unwrap();
1438
1439 assert_eq!(
1440 lines[0].layout.wrap_boundaries(),
1441 &[
1442 WrapBoundary {
1443 run_ix: 0,
1444 glyph_ix: 7
1445 },
1446 WrapBoundary {
1447 run_ix: 0,
1448 glyph_ix: 12
1449 },
1450 WrapBoundary {
1451 run_ix: 0,
1452 glyph_ix: 18
1453 }
1454 ],
1455 );
1456 });
1457 }
1458
1459 #[test]
1460 fn test_multiline_truncation_fits_within_wrapped_lines() {
1461 let mut wrapper = build_wrapper();
1462
1463 let text: &str = "aa bbbbbb cccccc dddddd eeee ffff";
1475 let wrap_width = px(72.);
1476 let max_lines: usize = 2;
1477
1478 let runs = generate_test_runs(&[text.len()]);
1479 let (truncated, _) = wrapper.truncate_wrapped_line(
1480 text.into(),
1481 wrap_width,
1482 max_lines,
1483 "\u{2026}",
1484 &runs,
1485 TruncateFrom::End,
1486 );
1487
1488 let wrap_count = wrapper
1490 .wrap_line(
1491 &[LineFragment::text(&truncated)],
1492 wrap_width,
1493 IndentAdjustment::default(),
1494 )
1495 .count();
1496
1497 assert!(
1498 wrap_count < max_lines,
1499 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1500 truncated,
1501 wrap_count + 1,
1502 max_lines
1503 );
1504
1505 assert!(
1507 truncated.ends_with('\u{2026}'),
1508 "Truncated text '{}' should end with ellipsis",
1509 truncated
1510 );
1511 }
1512
1513 #[test]
1514 fn test_multiline_truncation_no_truncation_needed() {
1515 let mut wrapper = build_wrapper();
1516
1517 let text: &str = "aa bbb cccccc";
1520 let wrap_width = px(72.);
1521 let max_lines: usize = 2;
1522
1523 let runs = generate_test_runs(&[text.len()]);
1524 let (result, _) = wrapper.truncate_wrapped_line(
1525 text.into(),
1526 wrap_width,
1527 max_lines,
1528 "\u{2026}",
1529 &runs,
1530 TruncateFrom::End,
1531 );
1532
1533 assert_eq!(
1534 result.as_ref(),
1535 text,
1536 "Text that fits should not be modified"
1537 );
1538 }
1539
1540 #[test]
1541 fn test_multiline_truncation_three_lines() {
1542 let mut wrapper = build_wrapper();
1543
1544 let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj";
1545 let wrap_width = px(72.);
1546 let max_lines: usize = 3;
1547
1548 let runs = generate_test_runs(&[text.len()]);
1549 let (truncated, _) = wrapper.truncate_wrapped_line(
1550 text.into(),
1551 wrap_width,
1552 max_lines,
1553 "\u{2026}",
1554 &runs,
1555 TruncateFrom::End,
1556 );
1557
1558 let wrap_count = wrapper
1559 .wrap_line(
1560 &[LineFragment::text(&truncated)],
1561 wrap_width,
1562 IndentAdjustment::default(),
1563 )
1564 .count();
1565
1566 assert!(
1567 wrap_count < max_lines,
1568 "Truncated text '{}' wraps into {} visual lines, expected at most {}",
1569 truncated,
1570 wrap_count + 1,
1571 max_lines
1572 );
1573
1574 assert!(
1575 truncated.ends_with('\u{2026}'),
1576 "Truncated text '{}' should end with ellipsis",
1577 truncated
1578 );
1579 }
1580
1581 #[test]
1582 fn test_multiline_truncation_preserves_breaks_after_empty_lines() {
1583 let mut wrapper = build_wrapper();
1584 for prefix in ["", "\n", "\n\n", "hello\n\n"] {
1585 let text = format!("{prefix}aaa required! more words follow");
1586 let runs = generate_test_runs(&[text.len()]);
1587 let max_lines = prefix.bytes().filter(|byte| *byte == b'\n').count() + 2;
1588 let (truncated, _) = wrapper.truncate_wrapped_line(
1589 text.into(),
1590 px(72.),
1591 max_lines,
1592 "…",
1593 &runs,
1594 TruncateFrom::End,
1595 );
1596 assert_eq!(
1597 truncated.as_ref(),
1598 format!("{prefix}aaa {}…", &"required!"[..6]),
1599 "prefix: {prefix:?}",
1600 );
1601 }
1602 }
1603
1604 #[test]
1605 fn test_multiline_truncation_with_newlines() {
1606 let mut wrapper = build_wrapper();
1607
1608 let text: &str = "hello\nworld foo bar baz";
1613 let wrap_width = px(72.);
1614 let max_lines: usize = 2;
1615
1616 let runs = generate_test_runs(&[text.len()]);
1617 let (truncated, _) = wrapper.truncate_wrapped_line(
1618 text.into(),
1619 wrap_width,
1620 max_lines,
1621 "\u{2026}",
1622 &runs,
1623 TruncateFrom::End,
1624 );
1625
1626 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1628 assert_eq!(
1629 parts.len(),
1630 2,
1631 "Newline should be preserved: '{}'",
1632 truncated
1633 );
1634 assert_eq!(parts[0], "hello");
1635
1636 let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum();
1638 assert!(
1639 second_line_width <= wrap_width,
1640 "Second line '{}' ({}px) exceeds wrap_width ({}px)",
1641 parts[1],
1642 second_line_width,
1643 wrap_width
1644 );
1645 assert!(
1646 truncated.ends_with('\u{2026}'),
1647 "Should end with ellipsis: '{}'",
1648 truncated
1649 );
1650 }
1651
1652 #[test]
1653 fn test_multiline_truncation_newline_on_last_line() {
1654 let mut wrapper = build_wrapper();
1655
1656 let text: &str = "hello\nworld\nmore";
1660 let wrap_width = px(72.);
1661 let max_lines: usize = 2;
1662
1663 let runs = generate_test_runs(&[text.len()]);
1664 let (truncated, _) = wrapper.truncate_wrapped_line(
1665 text.into(),
1666 wrap_width,
1667 max_lines,
1668 "\u{2026}",
1669 &runs,
1670 TruncateFrom::End,
1671 );
1672
1673 let parts: Vec<&str> = truncated.splitn(2, '\n').collect();
1674 assert_eq!(parts[0], "hello");
1675 assert!(
1676 truncated.ends_with('\u{2026}'),
1677 "Should end with ellipsis since there's more content: '{}'",
1678 truncated
1679 );
1680 }
1681
1682 #[test]
1683 fn test_truncate_line_middle() {
1684 let mut wrapper = build_wrapper();
1685
1686 let short_text = "hello world";
1688 let runs = generate_test_runs(&[short_text.len()]);
1689 let (result, result_runs) = wrapper.truncate_line(
1690 short_text.into(),
1691 px(10000.),
1692 "…",
1693 &runs,
1694 TruncateFrom::Middle,
1695 );
1696 assert_eq!(result.as_ref(), short_text);
1697 assert_eq!(result_runs.len(), 1);
1698 assert_eq!(result_runs[0].len, short_text.len());
1699
1700 let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1702 let runs = generate_test_runs(&[long_text.len()]);
1703 let (result, _result_runs) =
1704 wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle);
1705 assert!(
1706 result.contains('…'),
1707 "Middle-truncated result should contain '…', got: '{}'",
1708 result
1709 );
1710 assert!(
1711 result.chars().count() < long_text.chars().count(),
1712 "Middle-truncated result should be shorter than original"
1713 );
1714 assert_eq!(
1715 result.chars().next(),
1716 long_text.chars().next(),
1717 "Result should start with the same first character as original"
1718 );
1719 assert_eq!(
1720 result.chars().last(),
1721 long_text.chars().last(),
1722 "Result should end with the same last character as original"
1723 );
1724
1725 let text = "abcdef";
1728 let runs = generate_test_runs(&[text.len()]);
1729 let (result, result_runs) =
1730 wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle);
1731 assert_eq!(result.as_ref(), "…");
1732 assert_eq!(result_runs.len(), 1);
1733 assert_eq!(result_runs[0].len, "…".len());
1734
1735 let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz";
1738 let run_lens = [20, 20, multi_run_text.len() - 40];
1739 let runs = generate_test_runs(&run_lens);
1740 let (result, result_runs) = wrapper.truncate_line(
1741 multi_run_text.into(),
1742 px(100.),
1743 "…",
1744 &runs,
1745 TruncateFrom::Middle,
1746 );
1747 let total_run_len: usize = result_runs.iter().map(|r| r.len).sum();
1748 assert_eq!(
1749 total_run_len,
1750 result.len(),
1751 "Sum of run lengths ({}) should equal result byte length ({})",
1752 total_run_len,
1753 result.len()
1754 );
1755 }
1756
1757 #[test]
1758 fn test_multiline_truncation_trailing_newline() {
1759 let mut wrapper = build_wrapper();
1760
1761 let text: &str = "hello\nworld\n";
1764 let wrap_width = px(72.);
1765 let max_lines: usize = 2;
1766
1767 let runs = generate_test_runs(&[text.len()]);
1768 let (result, _) = wrapper.truncate_wrapped_line(
1769 text.into(),
1770 wrap_width,
1771 max_lines,
1772 "\u{2026}",
1773 &runs,
1774 TruncateFrom::End,
1775 );
1776
1777 assert!(
1778 !result.ends_with('\u{2026}'),
1779 "Trailing newline with no content should not add ellipsis: '{}'",
1780 result
1781 );
1782 }
1783
1784 #[test]
1785 fn test_multiline_truncation_newline_fits_exactly() {
1786 let mut wrapper = build_wrapper();
1787
1788 let text: &str = "hello\nworld";
1791 let wrap_width = px(72.);
1792 let max_lines: usize = 2;
1793
1794 let runs = generate_test_runs(&[text.len()]);
1795 let (result, _) = wrapper.truncate_wrapped_line(
1796 text.into(),
1797 wrap_width,
1798 max_lines,
1799 "\u{2026}",
1800 &runs,
1801 TruncateFrom::End,
1802 );
1803
1804 assert_eq!(
1805 result.as_ref(),
1806 text,
1807 "Text that fits exactly should not be modified: '{}'",
1808 result
1809 );
1810 }
1811
1812 #[test]
1813 fn test_extra_columns_overflow_guard() {
1814 let mut wrapper = build_wrapper();
1815 let space_width = wrapper.width_for_char(' ');
1816
1817 let text = " ab cd ef gh";
1819 let wrap_width = space_width * 10.0;
1820
1821 assert_eq!(
1828 wrapper
1829 .wrap_line(
1830 &[LineFragment::text(text)],
1831 wrap_width,
1832 IndentAdjustment::ExtraColumns(8),
1833 )
1834 .collect::<Vec<_>>(),
1835 &[Boundary::new(9, 0)]
1836 );
1837
1838 assert_eq!(
1851 wrapper
1852 .wrap_line(
1853 &[LineFragment::text(text)],
1854 wrap_width,
1855 IndentAdjustment::ExtraColumns(2),
1856 )
1857 .collect::<Vec<_>>(),
1858 &[
1859 Boundary::new(9, 8),
1860 Boundary::new(11, 8),
1861 Boundary::new(12, 8),
1862 Boundary::new(14, 8),
1863 Boundary::new(15, 8),
1864 ]
1865 );
1866
1867 let full_width_text = " 🦀🦀🦀🦀🦀🦀🦀🦀";
1880 assert_eq!(
1881 wrapper
1882 .wrap_line(
1883 &[LineFragment::text(full_width_text)],
1884 wrap_width,
1885 IndentAdjustment::ExtraColumns(8),
1886 )
1887 .collect::<Vec<_>>(),
1888 &[Boundary::new(17, 0)]
1889 );
1890
1891 assert_eq!(
1898 wrapper
1899 .wrap_line(
1900 &[LineFragment::text("a abcdefghi")],
1901 wrap_width,
1902 IndentAdjustment::ExtraColumns(1),
1903 )
1904 .collect::<Vec<_>>(),
1905 &[Boundary::new(2, 1)]
1906 );
1907
1908 assert_eq!(
1915 wrapper
1916 .wrap_line(
1917 &[LineFragment::text("a abcdefghi")],
1918 wrap_width,
1919 IndentAdjustment::ExtraColumns(2),
1920 )
1921 .collect::<Vec<_>>(),
1922 &[Boundary::new(2, 0)]
1923 );
1924
1925 let carried_text = "a abcdefghij";
1937 assert_eq!(
1938 wrapper
1939 .wrap_line(
1940 &[LineFragment::text(carried_text)],
1941 wrap_width,
1942 IndentAdjustment::ExtraColumns(8),
1943 )
1944 .collect::<Vec<_>>(),
1945 &[Boundary::new(2, 0)]
1946 );
1947
1948 let element_fragments = [
1966 LineFragment::text("abcdefghijk "),
1967 LineFragment::element(space_width * 8.0, 1),
1968 LineFragment::text("z 12"),
1969 ];
1970 assert_eq!(
1971 wrapper
1972 .wrap_line(
1973 &element_fragments,
1974 wrap_width,
1975 IndentAdjustment::ExtraColumns(8),
1976 )
1977 .collect::<Vec<_>>(),
1978 &[
1979 Boundary::new(10, 8),
1980 Boundary::new(12, 0),
1981 Boundary::new(15, 8),
1982 ]
1983 );
1984
1985 let spaces = " "; assert_eq!(
1989 wrapper
1990 .wrap_line(
1991 &[LineFragment::text(spaces)],
1992 wrap_width,
1993 IndentAdjustment::ExtraColumns(8),
1994 )
1995 .collect::<Vec<_>>(),
1996 &[Boundary::new(10, 0)]
1997 );
1998
1999 let multiline_indent_text = " ab cd ef gh";
2008 assert_eq!(
2009 wrapper
2010 .wrap_line(
2011 &[LineFragment::text(multiline_indent_text)],
2012 wrap_width,
2013 IndentAdjustment::ExtraColumns(2),
2014 )
2015 .collect::<Vec<_>>(),
2016 &[
2017 Boundary::new(10, 0),
2018 Boundary::new(20, 6),
2019 Boundary::new(23, 6),
2020 ]
2021 );
2022
2023 let oversized_suffix_fragments = [
2040 LineFragment::text("aaaaaaaaaaaaaaaaaaa a"),
2041 LineFragment::element(space_width * 10.0, 1),
2042 ];
2043 assert_eq!(
2044 wrapper
2045 .wrap_line(
2046 &oversized_suffix_fragments,
2047 wrap_width,
2048 IndentAdjustment::ExtraColumns(2),
2049 )
2050 .collect::<Vec<_>>(),
2051 &[
2052 Boundary::new(10, 2),
2053 Boundary::new(18, 2),
2054 Boundary::new(21, 0),
2055 ]
2056 );
2057
2058 assert_eq!(
2059 wrapper
2060 .wrap_line(
2061 &[LineFragment::text(" aaaaaaaa")],
2062 space_width * 4.0,
2063 IndentAdjustment::ExtraColumns(u32::MAX),
2064 )
2065 .collect::<Vec<_>>(),
2066 &[Boundary::new(4, 0), Boundary::new(8, 0)]
2067 );
2068 }
2069}