Skip to main content

gpui/text_system/
line_wrapper.rs

1use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4use unicode_segmentation::UnicodeSegmentation;
5
6/// Determines whether to truncate text from the start or end.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum TruncateFrom {
9    /// Truncate text from the start.
10    Start,
11    /// Truncate text from the end.
12    End,
13    /// Truncate text from the middle, preserving the start and end.
14    Middle,
15}
16
17/// Controls how soft-wrapped continuation lines are indented.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19pub enum IndentAdjustment {
20    /// No indent - continuation lines start at column 0.
21    NoIndent,
22    /// Match the original line's leading whitespace.
23    #[default]
24    SameIndent,
25    /// Add N extra columns of indent (in space-character widths).
26    ExtraColumns(u32),
27}
28
29/// The GPUI line wrapper, used to wrap lines of text to a given width.
30pub 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
38/// Cached character widths for one font and size.
39pub(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    /// The maximum indent that can be applied to a line.
55    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    /// Wrap a line of text to the given width with this wrapper's font and font size.
77    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                // This placeholder keeps inline elements attached to adjacent words
104                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                    // Compute base indentation from the first non-whitespace character on the line
179                    // and retain it for all subsequent wrap rows. If the line begins with leading
180                    // whitespace that wraps before any non-whitespace character (or is all whitespace),
181                    // base_indent remains None so continuation rows within the leading whitespace
182                    // do not receive ExtraColumns indentation and degrade into cascading short rows.
183                    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                                // Reserve headroom for any carried suffix from an earlier word boundary
200                                // (and at least 2 columns for a full-width character) so the continuation
201                                // line does not immediately exceed wrap width.
202                                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                        // An oversized grapheme stays intact, even if it exceeds the line width.
220                        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    /// Determines if a line should be truncated based on its width.
239    ///
240    /// Returns the truncation index in `line`.
241    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    /// Truncate a line of text to the given width with this wrapper's font and font size.
348    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    /// Truncate text to fit within a given number of wrapped lines.
402    ///
403    /// Unlike `truncate_line` which treats the text as a flat width budget
404    /// (`width * max_lines`), this method accounts for word-boundary wrapping:
405    /// it walks through characters once, tracking wrap boundaries and the
406    /// truncation point simultaneously. When text overflows on the last
407    /// allowed line, it truncates there and appends the affix.
408    ///
409    /// For `max_lines == 1`, this delegates to `truncate_line`.
410    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            // Consume boundaries on newlines too, so skipping an empty line does not
449            // leave the iterator stuck before subsequent text.
450            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                    // Newline on the last allowed line with real content
454                    // below. Truncate here.
455                    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                // Newline before the last line: it consumes a line.
469                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                // Before the last line: replicate wrap_line's boundary logic.
495                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                // On the last line: track the furthest point where the affix
518                // still fits, and stop as soon as the line overflows.
519                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        // Text fits within max_lines without truncation.
540        drop(break_indices);
541        (text, Cow::Borrowed(runs))
542    }
543
544    /// The byte offsets in `text` at which a line break is allowed.
545    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    // Front segment [0, front_end_ix) + ellipsis: walk forward until the run
633    // that straddles or ends at front_end_ix, then extend that run's length
634    // to include the ellipsis.
635    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        // front_end_ix landed exactly on a run boundary; append ellipsis to
653        // the last front run (or, if the front is empty, to the first back run).
654        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    // Back segment [back_start_ix, original.len()): skip runs entirely in the
664    // removed middle, keep the rest.
665    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                // Run straddles back_start_ix; keep only the tail.
671                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
684/// A fragment of a line that can be wrapped.
685pub enum LineFragment<'a> {
686    /// A text fragment consisting of characters.
687    Text {
688        /// The text content of the fragment.
689        text: &'a str,
690    },
691    /// A non-text element with a fixed width.
692    Element {
693        /// The width of the element in pixels.
694        width: Pixels,
695        /// The UTF-8 encoded length of the element.
696        len_utf8: usize,
697    },
698}
699
700impl<'a> LineFragment<'a> {
701    /// Creates a new text fragment from the given text.
702    pub fn text(text: &'a str) -> Self {
703        LineFragment::Text { text }
704    }
705
706    /// Creates a new non-text element with the given width and UTF-8 encoded length.
707    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/// A boundary between two lines of text.
744#[derive(Copy, Clone, Debug, PartialEq, Eq)]
745pub struct Boundary {
746    /// The index of the last character in a line
747    pub ix: usize,
748    /// The indent of the next line.
749    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        // Test wrapping multiple text fragments
1019        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        // Test wrapping with a mix of text and element fragments
1038        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        // Test with element at the beginning and text afterward
1060        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        // Test with a large element that forces wrapping by itself
1080        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        // Test with non-breaking glue characters
1101        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),], // special chars above take up 3, 2 and 3 bytes, so boundary ends up at 12
1110        );
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        // Case 0: Normal
1262        // Text: abcdefghijkl
1263        // Runs: Run0 { len: 12, ... }
1264        //
1265        // Truncate res: abcd… (truncate_at = 4)
1266        // Run res: Run0 { string: abcd…, len: 7, ... }
1267        perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1268        // Case 1: Drop some runs
1269        // Text: abcdefghijkl
1270        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1271        //
1272        // Truncate res: abcdef… (truncate_at = 6)
1273        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
1274        // 5, ... }
1275        perform_test(
1276            &mut wrapper,
1277            "abcdefghijkl",
1278            "abcdef…",
1279            &[4, 4, 4],
1280            &[4, 5],
1281            px(70.),
1282        );
1283        // Case 2: Truncate at start of some run
1284        // Text: abcdefghijkl
1285        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1286        //
1287        // Truncate res: abcdefgh… (truncate_at = 8)
1288        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
1289        // 4, ... }, Run2 { string: …, len: 3, ... }
1290        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        // Case 0: Normal
1327        // Text: abcdefghijkl
1328        // Runs: Run0 { len: 12, ... }
1329        //
1330        // Truncate res: …ijkl (truncate_at = 9)
1331        // Run res: Run0 { string: …ijkl, len: 7, ... }
1332        perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1333        // Case 1: Drop some runs
1334        // Text: abcdefghijkl
1335        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1336        //
1337        // Truncate res: …ghijkl (truncate_at = 7)
1338        // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
1339        // 4, ... }
1340        perform_test(
1341            &mut wrapper,
1342            "abcdefghijkl",
1343            "…ghijkl",
1344            &[4, 4, 4],
1345            &[5, 4],
1346            px(70.),
1347        );
1348        // Case 2: Truncate at start of some run
1349        // Text: abcdefghijkl
1350        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1351        //
1352        // Truncate res: abcdefgh… (truncate_at = 3)
1353        // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
1354        // 4, ... }, Run2 { string: ijkl, len: 4, ... }
1355        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        // Case 0: Normal
1375        // Text: abcdefghijkl
1376        // Runs: Run0 { len: 12, ... }
1377        //
1378        // Truncate res: abcd… (truncate_at = 4)
1379        // Run res: Run0 { string: abcd…, len: 7, ... }
1380        perform_test("abcd…", &[12], &[7]);
1381        // Case 1: Drop some runs
1382        // Text: abcdefghijkl
1383        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1384        //
1385        // Truncate res: abcdef… (truncate_at = 6)
1386        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
1387        // 5, ... }
1388        perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1389        // Case 2: Truncate at start of some run
1390        // Text: abcdefghijkl
1391        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1392        //
1393        // Truncate res: abcdefgh… (truncate_at = 8)
1394        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
1395        // 4, ... }, Run2 { string: …, len: 3, ... }
1396        perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1397    }
1398
1399    // For compatibility with the test macro
1400    #[cfg(target_os = "macos")]
1401    use crate as gpui;
1402
1403    // These seem to vary wildly based on the text system.
1404    #[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        // With .ZedMono at 16px, each char is 9.6px wide.
1465        // wrap_width = 72px fits ~7 chars per line.
1466        //
1467        // "aa bbbbbb cccccc dddddd eeee ffff" with wrap_width=72px wraps as:
1468        //   Line 1: "aa "       (28.8px, wraps because "bbbbbb" won't fit)
1469        //   Line 2: "bbbbbb "   (67.2px)
1470        //   Line 3: "cccccc "   (67.2px)
1471        //   ...
1472        //
1473        // truncate_wrapped_line should wrap first to find line 2 starts at
1474        // "bbbbbb...", then truncate only that line to fit with ellipsis.
1475        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        // The truncated text, when wrapped, must fit within max_lines lines.
1490        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        // The truncated text should end with the ellipsis.
1507        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        // Text that fits in 2 lines shouldn't be truncated.
1519        // Line 1: "aa bbb " (67.2px), Line 2: "cccccc" (57.6px)
1520        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        // "hello\nworld foo bar baz" with line_clamp(2):
1610        // shape_text splits on \n, giving physical lines "hello" and
1611        // "world foo bar baz". The newline consumes line 1, so the
1612        // second physical line should be truncated on line 2.
1613        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        // The newline should be preserved.
1628        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        // The second line should fit within wrap_width and end with ellipsis.
1638        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        // "hello\nworld\nmore" with line_clamp(2):
1658        // Line 1: "hello", Line 2: "world" — but there's a third line,
1659        // so line 2 should be truncated with ellipsis.
1660        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        // No truncation when text fits within a very wide budget.
1688        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        // Basic middle truncation: long string with px(100.) budget.
1702        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        // Degenerate case: budget so narrow that middle truncation cannot find a valid split.
1727        // Still show the truncation affix instead of returning the original overflowing text.
1728        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        // Run adjustment correctness: multiple runs across the string.
1737        // Verify that the returned runs' lengths sum to result.len().
1738        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        // "hello\nworld\n" with line_clamp(2):
1763        // The trailing newline has no content after it, so no ellipsis.
1764        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        // "hello\nworld" with line_clamp(2):
1790        // Exactly 2 lines, no truncation needed.
1791        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        // 6 spaces indent, wrap width 10 columns.
1819        let text = "      ab cd ef gh";
1820        let wrap_width = space_width * 10.0;
1821
1822        // When base_indent + extra overflows wrap width (6 + 8 + 2 > 10),
1823        // indent must fall back to 0 instead of degrading to one character per row.
1824        //
1825        // Expected wrapped lines (10 columns):
1826        //   |      ab  |  (row 0: 6 spaces + "ab ", len 9)
1827        //   |cd ef gh  |  (row 1: 0 indent + "cd ef gh", len 8)
1828        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        // When base_indent + extra fits within wrap width (6 + 2 + 2 <= 10),
1840        // the extra indent is applied and not clamped.
1841        // Spaces count toward the width, so the separators occupy their own rows
1842        // rather than overflowing the preceding row or splitting the following word.
1843        //
1844        // Expected wrapped lines (10 columns):
1845        //   |      ab  |  (row 0: 6 spaces + "ab ", len 9)
1846        //   |        cd|  (row 1: 8 spaces + "cd", len 10)
1847        //   |          |  (row 2: 8 spaces + " ", len 9)
1848        //   |        ef|  (row 3: 8 spaces + "ef", len 10)
1849        //   |          |  (row 4: 8 spaces + " ", len 9)
1850        //   |        gh|  (row 5: 8 spaces + "gh", len 10)
1851        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        // When text contains full-width (two-column) glyphs, reserving headroom for a
1869        // two-column character (candidate + 2 > wrap_width) ensures that candidate
1870        // indents leaving only 1 column cannot accept the indent and overflow.
1871        //
1872        // 1 space indent, wrap width 10 columns.
1873        // " 🦀🦀🦀🦀🦀🦀🦀🦀" with extra=8 (extra_two with tab_size=4):
1874        // candidate = 1 + 8 = 9.
1875        // With +2 headroom (9 + 2 > 10), indent falls back to 0.
1876        //
1877        // Expected wrapped lines (10 columns):
1878        //   | 🦀🦀🦀🦀 |  (row 0: 1 space + 4 two-column glyphs, 9 cols)
1879        //   |🦀🦀🦀🦀  |  (row 1: 0 indent + 4 two-column glyphs, 8 cols)
1880        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        // When the carried suffix fits within wrap width alongside extra indent
1893        // (1 indent + 9 carried suffix = 10 <= 10), the extra indent is applied.
1894        //
1895        // Expected wrapped lines (10 columns):
1896        //   |a         |  (row 0: "a ", len 2)
1897        //   | abcdefghi|  (row 1: 1 indent + "abcdefghi", len 10)
1898        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        // When extra indent exceeds wrap width by even 1 column
1910        // (2 indent + 9 carried suffix = 11 > 10), indent falls back to 0.
1911        //
1912        // Expected wrapped lines (10 columns):
1913        //   |a         |  (row 0: "a ", len 2)
1914        //   |abcdefghi |  (row 1: 0 indent + "abcdefghi", len 9)
1915        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        // When text wraps at an earlier word boundary, the carried suffix
1927        // must be accounted for so indent + carried_suffix <= wrap_width.
1928        //
1929        // "a abcdefghij" with wrap_width 10 columns, ExtraColumns(8):
1930        // candidate = 0 + 8 = 8.
1931        // carried suffix = "abcdefghi" (9 columns).
1932        // 8 indent + 9 carried suffix = 17 > 10, so indent falls back to 0.
1933        //
1934        // Expected wrapped lines (10 columns):
1935        //   |a         |  (row 0: "a ", len 2)
1936        //   |abcdefghij|  (row 1: 0 indent + "abcdefghij", len 10)
1937        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        // When a wider inline element is encountered after the first wrap,
1950        // the overflow guard must evaluate whether the element fits with the extra indent.
1951        // If text continues after the element row and wraps again, subsequent continuation
1952        // lines resume the extra indent if their carried content fits.
1953        //
1954        // "abcdefghijk " followed by an 8-column element and "z 12":
1955        // Row 0: "abcdefghij" (len 10)
1956        // Row 1: "k " (2 cols) with 8 indent (len 10)
1957        // Row 2: element (8 cols) cannot fit with 8 indent (8 + 8 = 16 > 10),
1958        //        so indent falls back to 0. Element (8 cols) + "z " (2 cols) = len 10.
1959        // Row 3: "12" (2 cols) fits with 8 indent (8 + 2 = 10 <= 10).
1960        //
1961        // Expected wrapped lines (10 columns):
1962        //   |abcdefghij|  (row 0: 10 cols)
1963        //   |        k |  (row 1: 8 indent + "k ", len 10)
1964        //   |[ELEMENT]z|  (row 2: 0 indent + [ELEMENT (8)] + "z ", len 10)
1965        //   |        12|  (row 3: 8 indent + "12", len 10)
1966        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        // A line of only whitespace wrapping before any non-whitespace character
1987        // must not have extra columns added to subsequent rows.
1988        let spaces = "                    "; // 20 spaces
1989        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        // When leading whitespace wraps before the first non-whitespace character,
2001        // base_indent should reflect the leading whitespace on the row where non-whitespace begins.
2002        // 14 spaces followed by "ab cd ef gh", wrap width 10:
2003        // Row 0: 10 spaces (len 10) -> wraps at ix 10 with indent 0
2004        // Row 1: 4 spaces + "ab cd" (len 9) -> wraps at ix 20
2005        //        base_indent is 14 - 10 = 4.
2006        //        With ExtraColumns(2), candidate = 4 + 2 = 6. 6 + 2 (headroom) = 8 <= 10.
2007        //        Row 2 and subsequent rows get indent 6.
2008        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        // When a word boundary precedes an oversized carried suffix that itself
2025        // exceeds wrap_width (e.g. text followed by an inline element), wrapping
2026        // at the earlier candidate would force the continuation row to immediately
2027        // overflow even with 0 indent. The wrapper must reject the candidate and
2028        // break before the overflowing item instead.
2029        //
2030        // "aaaaaaaaaaaaaaaaaaa a" (21 chars: 19 'a's, space, 'a') followed by 10-column element:
2031        // Row 0: "aaaaaaaaaa" (10 chars, len 10) -> wraps at ix 10
2032        // Row 1: "aaaaaaaa" (8 chars) with 2 indent (len 10) -> wraps at ix 18
2033        // Row 2: "a a" (3 chars) with 2 indent (len 5)
2034        //        candidate boundary at ix 20 (space)
2035        //        Then element (10 cols): width becomes 5 + 10 = 15 > wrap_width (10).
2036        //        Candidate would carry 'a' (1 col) + element (10 cols) = 11 cols > 10.
2037        //        Because carried suffix (11) > wrap_width (10), candidate is rejected.
2038        //        Wrapper breaks at ix 21 (before element) with indent 0.
2039        // Row 3: element (10 cols, len 10)
2040        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}