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                    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        // Test wrapping multiple text fragments
1018        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        // Test wrapping with a mix of text and element fragments
1037        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        // Test with element at the beginning and text afterward
1059        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        // Test with a large element that forces wrapping by itself
1079        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        // Test with non-breaking glue characters
1100        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),], // special chars above take up 3, 2 and 3 bytes, so boundary ends up at 12
1109        );
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        // Case 0: Normal
1261        // Text: abcdefghijkl
1262        // Runs: Run0 { len: 12, ... }
1263        //
1264        // Truncate res: abcd… (truncate_at = 4)
1265        // Run res: Run0 { string: abcd…, len: 7, ... }
1266        perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
1267        // Case 1: Drop some runs
1268        // Text: abcdefghijkl
1269        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1270        //
1271        // Truncate res: abcdef… (truncate_at = 6)
1272        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
1273        // 5, ... }
1274        perform_test(
1275            &mut wrapper,
1276            "abcdefghijkl",
1277            "abcdef…",
1278            &[4, 4, 4],
1279            &[4, 5],
1280            px(70.),
1281        );
1282        // Case 2: Truncate at start of some run
1283        // Text: abcdefghijkl
1284        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1285        //
1286        // Truncate res: abcdefgh… (truncate_at = 8)
1287        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
1288        // 4, ... }, Run2 { string: …, len: 3, ... }
1289        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        // Case 0: Normal
1326        // Text: abcdefghijkl
1327        // Runs: Run0 { len: 12, ... }
1328        //
1329        // Truncate res: …ijkl (truncate_at = 9)
1330        // Run res: Run0 { string: …ijkl, len: 7, ... }
1331        perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
1332        // Case 1: Drop some runs
1333        // Text: abcdefghijkl
1334        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1335        //
1336        // Truncate res: …ghijkl (truncate_at = 7)
1337        // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
1338        // 4, ... }
1339        perform_test(
1340            &mut wrapper,
1341            "abcdefghijkl",
1342            "…ghijkl",
1343            &[4, 4, 4],
1344            &[5, 4],
1345            px(70.),
1346        );
1347        // Case 2: Truncate at start of some run
1348        // Text: abcdefghijkl
1349        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1350        //
1351        // Truncate res: abcdefgh… (truncate_at = 3)
1352        // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
1353        // 4, ... }, Run2 { string: ijkl, len: 4, ... }
1354        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        // Case 0: Normal
1374        // Text: abcdefghijkl
1375        // Runs: Run0 { len: 12, ... }
1376        //
1377        // Truncate res: abcd… (truncate_at = 4)
1378        // Run res: Run0 { string: abcd…, len: 7, ... }
1379        perform_test("abcd…", &[12], &[7]);
1380        // Case 1: Drop some runs
1381        // Text: abcdefghijkl
1382        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1383        //
1384        // Truncate res: abcdef… (truncate_at = 6)
1385        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
1386        // 5, ... }
1387        perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
1388        // Case 2: Truncate at start of some run
1389        // Text: abcdefghijkl
1390        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
1391        //
1392        // Truncate res: abcdefgh… (truncate_at = 8)
1393        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
1394        // 4, ... }, Run2 { string: …, len: 3, ... }
1395        perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
1396    }
1397
1398    // For compatibility with the test macro
1399    #[cfg(target_os = "macos")]
1400    use crate as gpui;
1401
1402    // These seem to vary wildly based on the text system.
1403    #[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        // With .ZedMono at 16px, each char is 9.6px wide.
1464        // wrap_width = 72px fits ~7 chars per line.
1465        //
1466        // "aa bbbbbb cccccc dddddd eeee ffff" with wrap_width=72px wraps as:
1467        //   Line 1: "aa "       (28.8px, wraps because "bbbbbb" won't fit)
1468        //   Line 2: "bbbbbb "   (67.2px)
1469        //   Line 3: "cccccc "   (67.2px)
1470        //   ...
1471        //
1472        // truncate_wrapped_line should wrap first to find line 2 starts at
1473        // "bbbbbb...", then truncate only that line to fit with ellipsis.
1474        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        // The truncated text, when wrapped, must fit within max_lines lines.
1489        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        // The truncated text should end with the ellipsis.
1506        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        // Text that fits in 2 lines shouldn't be truncated.
1518        // Line 1: "aa bbb " (67.2px), Line 2: "cccccc" (57.6px)
1519        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        // "hello\nworld foo bar baz" with line_clamp(2):
1609        // shape_text splits on \n, giving physical lines "hello" and
1610        // "world foo bar baz". The newline consumes line 1, so the
1611        // second physical line should be truncated on line 2.
1612        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        // The newline should be preserved.
1627        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        // The second line should fit within wrap_width and end with ellipsis.
1637        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        // "hello\nworld\nmore" with line_clamp(2):
1657        // Line 1: "hello", Line 2: "world" — but there's a third line,
1658        // so line 2 should be truncated with ellipsis.
1659        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        // No truncation when text fits within a very wide budget.
1687        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        // Basic middle truncation: long string with px(100.) budget.
1701        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        // Degenerate case: budget so narrow that middle truncation cannot find a valid split.
1726        // Still show the truncation affix instead of returning the original overflowing text.
1727        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        // Run adjustment correctness: multiple runs across the string.
1736        // Verify that the returned runs' lengths sum to result.len().
1737        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        // "hello\nworld\n" with line_clamp(2):
1762        // The trailing newline has no content after it, so no ellipsis.
1763        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        // "hello\nworld" with line_clamp(2):
1789        // Exactly 2 lines, no truncation needed.
1790        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        // 6 spaces indent, wrap width 10 columns.
1818        let text = "      ab cd ef gh";
1819        let wrap_width = space_width * 10.0;
1820
1821        // When base_indent + extra overflows wrap width (6 + 8 + 2 > 10),
1822        // indent must fall back to 0 instead of degrading to one character per row.
1823        //
1824        // Expected wrapped lines (10 columns):
1825        //   |      ab  |  (row 0: 6 spaces + "ab ", len 9)
1826        //   |cd ef gh  |  (row 1: 0 indent + "cd ef gh", len 8)
1827        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        // When base_indent + extra fits within wrap width (6 + 2 + 2 <= 10),
1839        // the extra indent is applied and not clamped.
1840        // Spaces count toward the width, so the separators occupy their own rows
1841        // rather than overflowing the preceding row or splitting the following word.
1842        //
1843        // Expected wrapped lines (10 columns):
1844        //   |      ab  |  (row 0: 6 spaces + "ab ", len 9)
1845        //   |        cd|  (row 1: 8 spaces + "cd", len 10)
1846        //   |          |  (row 2: 8 spaces + " ", len 9)
1847        //   |        ef|  (row 3: 8 spaces + "ef", len 10)
1848        //   |          |  (row 4: 8 spaces + " ", len 9)
1849        //   |        gh|  (row 5: 8 spaces + "gh", len 10)
1850        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        // When text contains full-width (two-column) glyphs, reserving headroom for a
1868        // two-column character (candidate + 2 > wrap_width) ensures that candidate
1869        // indents leaving only 1 column cannot accept the indent and overflow.
1870        //
1871        // 1 space indent, wrap width 10 columns.
1872        // " 🦀🦀🦀🦀🦀🦀🦀🦀" with extra=8 (extra_two with tab_size=4):
1873        // candidate = 1 + 8 = 9.
1874        // With +2 headroom (9 + 2 > 10), indent falls back to 0.
1875        //
1876        // Expected wrapped lines (10 columns):
1877        //   | 🦀🦀🦀🦀 |  (row 0: 1 space + 4 two-column glyphs, 9 cols)
1878        //   |🦀🦀🦀🦀  |  (row 1: 0 indent + 4 two-column glyphs, 8 cols)
1879        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        // When the carried suffix fits within wrap width alongside extra indent
1892        // (1 indent + 9 carried suffix = 10 <= 10), the extra indent is applied.
1893        //
1894        // Expected wrapped lines (10 columns):
1895        //   |a         |  (row 0: "a ", len 2)
1896        //   | abcdefghi|  (row 1: 1 indent + "abcdefghi", len 10)
1897        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        // When extra indent exceeds wrap width by even 1 column
1909        // (2 indent + 9 carried suffix = 11 > 10), indent falls back to 0.
1910        //
1911        // Expected wrapped lines (10 columns):
1912        //   |a         |  (row 0: "a ", len 2)
1913        //   |abcdefghi |  (row 1: 0 indent + "abcdefghi", len 9)
1914        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        // When text wraps at an earlier word boundary, the carried suffix
1926        // must be accounted for so indent + carried_suffix <= wrap_width.
1927        //
1928        // "a abcdefghij" with wrap_width 10 columns, ExtraColumns(8):
1929        // candidate = 0 + 8 = 8.
1930        // carried suffix = "abcdefghi" (9 columns).
1931        // 8 indent + 9 carried suffix = 17 > 10, so indent falls back to 0.
1932        //
1933        // Expected wrapped lines (10 columns):
1934        //   |a         |  (row 0: "a ", len 2)
1935        //   |abcdefghij|  (row 1: 0 indent + "abcdefghij", len 10)
1936        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        // When a wider inline element is encountered after the first wrap,
1949        // the overflow guard must evaluate whether the element fits with the extra indent.
1950        // If text continues after the element row and wraps again, subsequent continuation
1951        // lines resume the extra indent if their carried content fits.
1952        //
1953        // "abcdefghijk " followed by an 8-column element and "z 12":
1954        // Row 0: "abcdefghij" (len 10)
1955        // Row 1: "k " (2 cols) with 8 indent (len 10)
1956        // Row 2: element (8 cols) cannot fit with 8 indent (8 + 8 = 16 > 10),
1957        //        so indent falls back to 0. Element (8 cols) + "z " (2 cols) = len 10.
1958        // Row 3: "12" (2 cols) fits with 8 indent (8 + 2 = 10 <= 10).
1959        //
1960        // Expected wrapped lines (10 columns):
1961        //   |abcdefghij|  (row 0: 10 cols)
1962        //   |        k |  (row 1: 8 indent + "k ", len 10)
1963        //   |[ELEMENT]z|  (row 2: 0 indent + [ELEMENT (8)] + "z ", len 10)
1964        //   |        12|  (row 3: 8 indent + "12", len 10)
1965        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        // A line of only whitespace wrapping before any non-whitespace character
1986        // must not have extra columns added to subsequent rows.
1987        let spaces = "                    "; // 20 spaces
1988        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        // When leading whitespace wraps before the first non-whitespace character,
2000        // base_indent should reflect the leading whitespace on the row where non-whitespace begins.
2001        // 14 spaces followed by "ab cd ef gh", wrap width 10:
2002        // Row 0: 10 spaces (len 10) -> wraps at ix 10 with indent 0
2003        // Row 1: 4 spaces + "ab cd" (len 9) -> wraps at ix 20
2004        //        base_indent is 14 - 10 = 4.
2005        //        With ExtraColumns(2), candidate = 4 + 2 = 6. 6 + 2 (headroom) = 8 <= 10.
2006        //        Row 2 and subsequent rows get indent 6.
2007        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        // When a word boundary precedes an oversized carried suffix that itself
2024        // exceeds wrap_width (e.g. text followed by an inline element), wrapping
2025        // at the earlier candidate would force the continuation row to immediately
2026        // overflow even with 0 indent. The wrapper must reject the candidate and
2027        // break before the overflowing item instead.
2028        //
2029        // "aaaaaaaaaaaaaaaaaaa a" (21 chars: 19 'a's, space, 'a') followed by 10-column element:
2030        // Row 0: "aaaaaaaaaa" (10 chars, len 10) -> wraps at ix 10
2031        // Row 1: "aaaaaaaa" (8 chars) with 2 indent (len 10) -> wraps at ix 18
2032        // Row 2: "a a" (3 chars) with 2 indent (len 5)
2033        //        candidate boundary at ix 20 (space)
2034        //        Then element (10 cols): width becomes 5 + 10 = 15 > wrap_width (10).
2035        //        Candidate would carry 'a' (1 col) + element (10 cols) = 11 cols > 10.
2036        //        Because carried suffix (11) > wrap_width (10), candidate is rejected.
2037        //        Wrapper breaks at ix 21 (before element) with indent 0.
2038        // Row 3: element (10 cols, len 10)
2039        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}