Skip to main content

gpui_core/text_system/
line_wrapper.rs

1use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun, px};
2use collections::HashMap;
3use std::{iter, sync::Arc};
4
5/// The GPUI line wrapper, used to wrap lines of text to a given width.
6pub struct LineWrapper {
7    platform_text_system: Arc<dyn PlatformTextSystem>,
8    pub(crate) font_id: FontId,
9    pub(crate) font_size: Pixels,
10    cached_ascii_char_widths: [Option<Pixels>; 128],
11    cached_other_char_widths: HashMap<char, Pixels>,
12}
13
14impl LineWrapper {
15    /// The maximum indent that can be applied to a line.
16    pub const MAX_INDENT: u32 = 256;
17
18    pub(crate) fn new(
19        font_id: FontId,
20        font_size: Pixels,
21        text_system: Arc<dyn PlatformTextSystem>,
22    ) -> Self {
23        Self {
24            platform_text_system: text_system,
25            font_id,
26            font_size,
27            cached_ascii_char_widths: [None; 128],
28            cached_other_char_widths: HashMap::default(),
29        }
30    }
31
32    /// Wrap a line of text to the given width with this wrapper's font and font size.
33    pub fn wrap_line<'a>(
34        &'a mut self,
35        fragments: &'a [LineFragment],
36        wrap_width: Pixels,
37    ) -> impl Iterator<Item = Boundary> + 'a {
38        let mut width = px(0.);
39        let mut first_non_whitespace_ix = None;
40        let mut indent = None;
41        let mut last_candidate_ix = 0;
42        let mut last_candidate_width = px(0.);
43        let mut last_wrap_ix = 0;
44        let mut prev_c = '\0';
45        let mut index = 0;
46        let mut candidates = fragments
47            .iter()
48            .flat_map(move |fragment| fragment.wrap_boundary_candidates())
49            .peekable();
50        iter::from_fn(move || {
51            for candidate in candidates.by_ref() {
52                let ix = index;
53                index += candidate.len_utf8();
54                let mut new_prev_c = prev_c;
55                let item_width = match candidate {
56                    WrapBoundaryCandidate::Char { character: c } => {
57                        if c == '\n' {
58                            continue;
59                        }
60
61                        if Self::is_word_char(c) {
62                            if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
63                                last_candidate_ix = ix;
64                                last_candidate_width = width;
65                            }
66                        } else {
67                            // CJK may not be space separated, e.g.: `Hello world你好世界`
68                            if c != ' ' && first_non_whitespace_ix.is_some() {
69                                last_candidate_ix = ix;
70                                last_candidate_width = width;
71                            }
72                        }
73
74                        if c != ' ' && first_non_whitespace_ix.is_none() {
75                            first_non_whitespace_ix = Some(ix);
76                        }
77
78                        new_prev_c = c;
79
80                        self.width_for_char(c)
81                    }
82                    WrapBoundaryCandidate::Element {
83                        width: element_width,
84                        ..
85                    } => {
86                        if prev_c == ' ' && first_non_whitespace_ix.is_some() {
87                            last_candidate_ix = ix;
88                            last_candidate_width = width;
89                        }
90
91                        if first_non_whitespace_ix.is_none() {
92                            first_non_whitespace_ix = Some(ix);
93                        }
94
95                        element_width
96                    }
97                };
98
99                width += item_width;
100                if width > wrap_width && ix > last_wrap_ix {
101                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
102                    {
103                        indent = Some(
104                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
105                        );
106                    }
107
108                    if last_candidate_ix > 0 {
109                        last_wrap_ix = last_candidate_ix;
110                        width -= last_candidate_width;
111                        last_candidate_ix = 0;
112                    } else {
113                        last_wrap_ix = ix;
114                        width = item_width;
115                    }
116
117                    if let Some(indent) = indent {
118                        width += self.width_for_char(' ') * indent as f32;
119                    }
120
121                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
122                }
123
124                prev_c = new_prev_c;
125            }
126
127            None
128        })
129    }
130
131    /// Truncate a line of text to the given width with this wrapper's font and font size.
132    pub fn truncate_line(
133        &mut self,
134        line: SharedString,
135        truncate_width: Pixels,
136        truncation_suffix: &str,
137        runs: &mut Vec<TextRun>,
138    ) -> SharedString {
139        let mut width = px(0.);
140        let suffix_width = truncation_suffix
141            .chars()
142            .map(|c| self.width_for_char(c))
143            .fold(px(0.0), |a, x| a + x);
144        let mut truncate_ix = 0;
145        for (ix, c) in line.char_indices() {
146            if width + suffix_width < truncate_width {
147                truncate_ix = ix;
148            }
149
150            let char_width = self.width_for_char(c);
151            width += char_width;
152
153            if width.floor() > truncate_width {
154                let truncated = line[..truncate_ix]
155                    .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
156                let result = SharedString::from(format!("{}{}", truncated, truncation_suffix));
157                update_runs_after_truncation(&result, truncation_suffix, runs);
158
159                return result;
160            }
161        }
162
163        line
164    }
165
166    pub(crate) fn is_word_char(c: char) -> bool {
167        // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
168        c.is_ascii_alphanumeric() ||
169        // Latin script in Unicode for French, German, Spanish, etc.
170        // Latin-1 Supplement
171        // https://en.wikipedia.org/wiki/Latin-1_Supplement
172        matches!(c, '\u{00C0}'..='\u{00FF}') ||
173        // Latin Extended-A
174        // https://en.wikipedia.org/wiki/Latin_Extended-A
175        matches!(c, '\u{0100}'..='\u{017F}') ||
176        // Latin Extended-B
177        // https://en.wikipedia.org/wiki/Latin_Extended-B
178        matches!(c, '\u{0180}'..='\u{024F}') ||
179        // Cyrillic for Russian, Ukrainian, etc.
180        // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
181        matches!(c, '\u{0400}'..='\u{04FF}') ||
182        // Some other known special characters that should be treated as word characters,
183        // e.g. `a-b`, `var_name`, `I'm`/`won't`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc.
184        matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!' | ';' | '*') ||
185        // Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL.
186        matches!(c,  '/' | ':' | '?' | '&' | '=') ||
187        // Closing punctuation never starts a line (UAX #14 LB13: no break
188        // before `!`, `)`, `]`, `}`, closing quotes or an ellipsis) — `plz!`,
189        // `see)`, `quoted”` wrap as one word instead of orphaning the mark on
190        // the next line. `/` and `?` stay local URL-glue characters so long
191        // paths and query strings (`a/b`, `foo?b=2`) still wrap as one token.
192        matches!(c, ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
193        // `⋯` character is special used in Zed, to keep this at the end of the line.
194        matches!(c, '⋯') ||
195        // Non-breaking glue characters.
196        matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
197    }
198
199    #[inline(always)]
200    fn width_for_char(&mut self, c: char) -> Pixels {
201        if (c as u32) < 128 {
202            if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
203                cached_width
204            } else {
205                let width = self.compute_width_for_char(c);
206                self.cached_ascii_char_widths[c as usize] = Some(width);
207                width
208            }
209        } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
210            *cached_width
211        } else {
212            let width = self.compute_width_for_char(c);
213            self.cached_other_char_widths.insert(c, width);
214            width
215        }
216    }
217
218    fn compute_width_for_char(&self, c: char) -> Pixels {
219        let mut buffer = [0; 4];
220        let buffer = c.encode_utf8(&mut buffer);
221        self.platform_text_system
222            .layout_line(
223                buffer,
224                self.font_size,
225                &[FontRun {
226                    len: buffer.len(),
227                    font_id: self.font_id,
228                }],
229            )
230            .width
231    }
232}
233
234fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
235    let mut truncate_at = result.len() - ellipsis.len();
236    let mut run_end = None;
237    for (run_index, run) in runs.iter_mut().enumerate() {
238        if run.len <= truncate_at {
239            truncate_at -= run.len;
240        } else {
241            run.len = truncate_at + ellipsis.len();
242            run_end = Some(run_index + 1);
243            break;
244        }
245    }
246    if let Some(run_end) = run_end {
247        runs.truncate(run_end);
248    }
249}
250
251/// A fragment of a line that can be wrapped.
252pub enum LineFragment<'a> {
253    /// A text fragment consisting of characters.
254    Text {
255        /// The text content of the fragment.
256        text: &'a str,
257    },
258    /// A non-text element with a fixed width.
259    Element {
260        /// The width of the element in pixels.
261        width: Pixels,
262        /// The UTF-8 encoded length of the element.
263        len_utf8: usize,
264    },
265}
266
267impl<'a> LineFragment<'a> {
268    /// Creates a new text fragment from the given text.
269    pub fn text(text: &'a str) -> Self {
270        LineFragment::Text { text }
271    }
272
273    /// Creates a new non-text element with the given width and UTF-8 encoded length.
274    pub fn element(width: Pixels, len_utf8: usize) -> Self {
275        LineFragment::Element { width, len_utf8 }
276    }
277
278    fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
279        let text = match self {
280            LineFragment::Text { text } => text,
281            LineFragment::Element { .. } => "\0",
282        };
283        text.chars().map(move |character| {
284            if let LineFragment::Element { width, len_utf8 } = self {
285                WrapBoundaryCandidate::Element {
286                    width: *width,
287                    len_utf8: *len_utf8,
288                }
289            } else {
290                WrapBoundaryCandidate::Char { character }
291            }
292        })
293    }
294}
295
296enum WrapBoundaryCandidate {
297    Char { character: char },
298    Element { width: Pixels, len_utf8: usize },
299}
300
301impl WrapBoundaryCandidate {
302    pub fn len_utf8(&self) -> usize {
303        match self {
304            WrapBoundaryCandidate::Char { character } => character.len_utf8(),
305            WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
306        }
307    }
308}
309
310/// A boundary between two lines of text.
311#[derive(Copy, Clone, Debug, PartialEq, Eq)]
312pub struct Boundary {
313    /// The index of the last character in a line
314    pub ix: usize,
315    /// The indent of the next line.
316    pub next_indent: u32,
317}
318
319impl Boundary {
320    fn new(ix: usize, next_indent: u32) -> Self {
321        Self { ix, next_indent }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::{
329        Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, font,
330    };
331    #[cfg(target_os = "macos")]
332    use crate::{TextRun, WindowTextSystem, WrapBoundary};
333    use rand::prelude::*;
334
335    fn build_wrapper() -> LineWrapper {
336        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
337        let cx = TestAppContext::build(dispatcher, None);
338        let id = cx.text_system().resolve_font(&font(".ZedMono"));
339        LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
340    }
341
342    fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
343        input_run_len
344            .iter()
345            .map(|run_len| TextRun {
346                len: *run_len,
347                font: Font {
348                    family: "Dummy".into(),
349                    features: FontFeatures::default(),
350                    fallbacks: None,
351                    weight: FontWeight::default(),
352                    style: FontStyle::Normal,
353                },
354                color: Hsla::default(),
355                background_color: None,
356                underline: None,
357                strikethrough: None,
358            })
359            .collect()
360    }
361
362    #[test]
363    fn test_wrap_line() {
364        let mut wrapper = build_wrapper();
365
366        assert_eq!(
367            wrapper
368                .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
369                .collect::<Vec<_>>(),
370            &[
371                Boundary::new(7, 0),
372                Boundary::new(12, 0),
373                Boundary::new(18, 0)
374            ],
375        );
376        assert_eq!(
377            wrapper
378                .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
379                .collect::<Vec<_>>(),
380            &[
381                Boundary::new(4, 0),
382                Boundary::new(11, 0),
383                Boundary::new(18, 0)
384            ],
385        );
386        assert_eq!(
387            wrapper
388                .wrap_line(&[LineFragment::text("     aaaaaaa")], px(72.))
389                .collect::<Vec<_>>(),
390            &[
391                Boundary::new(7, 5),
392                Boundary::new(9, 5),
393                Boundary::new(11, 5),
394            ]
395        );
396        assert_eq!(
397            wrapper
398                .wrap_line(
399                    &[LineFragment::text("                            ")],
400                    px(72.)
401                )
402                .collect::<Vec<_>>(),
403            &[
404                Boundary::new(7, 0),
405                Boundary::new(14, 0),
406                Boundary::new(21, 0)
407            ]
408        );
409        assert_eq!(
410            wrapper
411                .wrap_line(&[LineFragment::text("          aaaaaaaaaaaaaa")], px(72.))
412                .collect::<Vec<_>>(),
413            &[
414                Boundary::new(7, 0),
415                Boundary::new(14, 3),
416                Boundary::new(18, 3),
417                Boundary::new(22, 3),
418            ]
419        );
420
421        // Test wrapping multiple text fragments
422        assert_eq!(
423            wrapper
424                .wrap_line(
425                    &[
426                        LineFragment::text("aa bbb "),
427                        LineFragment::text("cccc ddddd eeee")
428                    ],
429                    px(72.)
430                )
431                .collect::<Vec<_>>(),
432            &[
433                Boundary::new(7, 0),
434                Boundary::new(12, 0),
435                Boundary::new(18, 0)
436            ],
437        );
438
439        // Test wrapping with a mix of text and element fragments
440        assert_eq!(
441            wrapper
442                .wrap_line(
443                    &[
444                        LineFragment::text("aa "),
445                        LineFragment::element(px(20.), 1),
446                        LineFragment::text(" bbb "),
447                        LineFragment::element(px(30.), 1),
448                        LineFragment::text(" cccc")
449                    ],
450                    px(72.)
451                )
452                .collect::<Vec<_>>(),
453            &[
454                Boundary::new(5, 0),
455                Boundary::new(9, 0),
456                Boundary::new(11, 0)
457            ],
458        );
459
460        // Test with element at the beginning and text afterward
461        assert_eq!(
462            wrapper
463                .wrap_line(
464                    &[
465                        LineFragment::element(px(50.), 1),
466                        LineFragment::text(" aaaa bbbb cccc dddd")
467                    ],
468                    px(72.)
469                )
470                .collect::<Vec<_>>(),
471            &[
472                Boundary::new(2, 0),
473                Boundary::new(7, 0),
474                Boundary::new(12, 0),
475                Boundary::new(17, 0)
476            ],
477        );
478
479        // Test with a large element that forces wrapping by itself
480        assert_eq!(
481            wrapper
482                .wrap_line(
483                    &[
484                        LineFragment::text("short text "),
485                        LineFragment::element(px(100.), 1),
486                        LineFragment::text(" more text")
487                    ],
488                    px(72.)
489                )
490                .collect::<Vec<_>>(),
491            &[
492                Boundary::new(6, 0),
493                Boundary::new(11, 0),
494                Boundary::new(12, 0),
495                Boundary::new(18, 0)
496            ],
497        );
498    }
499
500    #[test]
501    fn test_truncate_line() {
502        let mut wrapper = build_wrapper();
503
504        fn perform_test(
505            wrapper: &mut LineWrapper,
506            text: &'static str,
507            result: &'static str,
508            ellipsis: &str,
509        ) {
510            perform_test_with_width(wrapper, text, result, ellipsis, px(220.));
511        }
512
513        fn perform_test_with_width(
514            wrapper: &mut LineWrapper,
515            text: &'static str,
516            result: &'static str,
517            ellipsis: &str,
518            width: Pixels,
519        ) {
520            let dummy_run_lens = vec![text.len()];
521            let mut dummy_runs = generate_test_runs(&dummy_run_lens);
522            assert_eq!(
523                wrapper.truncate_line(text.into(), width, ellipsis, &mut dummy_runs),
524                result
525            );
526            assert_eq!(dummy_runs.first().unwrap().len, result.len());
527        }
528
529        perform_test(
530            &mut wrapper,
531            "aa bbb cccc ddddd eeee ffff gggg",
532            "aa bbb cccc ddddd eeee",
533            "",
534        );
535        perform_test(
536            &mut wrapper,
537            "aa bbb cccc ddddd eeee ffff gggg",
538            "aa bbb cccc ddddd eee…",
539            "…",
540        );
541        perform_test(
542            &mut wrapper,
543            "aa bbb cccc ddddd eeee ffff gggg",
544            "aa bbb cccc dddd......",
545            "......",
546        );
547        perform_test_with_width(
548            &mut wrapper,
549            "aa bbb cccc ddddd. eeee ffff gggg",
550            "aa bbb cccc ddddd…",
551            "…",
552            px(195.),
553        );
554        perform_test_with_width(
555            &mut wrapper,
556            "aa bbb cccc ddddd  eeee ffff gggg",
557            "aa bbb cccc ddddd…",
558            "…",
559            px(195.),
560        );
561    }
562
563    #[test]
564    fn test_truncate_multiple_runs() {
565        let mut wrapper = build_wrapper();
566
567        fn perform_test(
568            wrapper: &mut LineWrapper,
569            text: &'static str,
570            result: &str,
571            run_lens: &[usize],
572            result_run_len: &[usize],
573            line_width: Pixels,
574        ) {
575            let mut dummy_runs = generate_test_runs(run_lens);
576            assert_eq!(
577                wrapper.truncate_line(text.into(), line_width, "…", &mut dummy_runs),
578                result
579            );
580            for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
581                assert_eq!(run.len, *result_len);
582            }
583        }
584        // Case 0: Normal
585        // Text: abcdefghijkl
586        // Runs: Run0 { len: 12, ... }
587        //
588        // Truncate res: abcd… (truncate_at = 4)
589        // Run res: Run0 { string: abcd…, len: 7, ... }
590        perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
591        // Case 1: Drop some runs
592        // Text: abcdefghijkl
593        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
594        //
595        // Truncate res: abcdef… (truncate_at = 6)
596        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
597        // 5, ... }
598        perform_test(
599            &mut wrapper,
600            "abcdefghijkl",
601            "abcdef…",
602            &[4, 4, 4],
603            &[4, 5],
604            px(70.),
605        );
606        // Case 2: Truncate at start of some run
607        // Text: abcdefghijkl
608        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
609        //
610        // Truncate res: abcdefgh… (truncate_at = 8)
611        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
612        // 4, ... }, Run2 { string: …, len: 3, ... }
613        perform_test(
614            &mut wrapper,
615            "abcdefghijkl",
616            "abcdefgh…",
617            &[4, 4, 4],
618            &[4, 4, 3],
619            px(90.),
620        );
621    }
622
623    #[test]
624    fn test_update_run_after_truncation() {
625        fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
626            let mut dummy_runs = generate_test_runs(run_lens);
627            update_runs_after_truncation(result, "…", &mut dummy_runs);
628            for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
629                assert_eq!(run.len, *result_len);
630            }
631        }
632        // Case 0: Normal
633        // Text: abcdefghijkl
634        // Runs: Run0 { len: 12, ... }
635        //
636        // Truncate res: abcd… (truncate_at = 4)
637        // Run res: Run0 { string: abcd…, len: 7, ... }
638        perform_test("abcd…", &[12], &[7]);
639        // Case 1: Drop some runs
640        // Text: abcdefghijkl
641        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
642        //
643        // Truncate res: abcdef… (truncate_at = 6)
644        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
645        // 5, ... }
646        perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
647        // Case 2: Truncate at start of some run
648        // Text: abcdefghijkl
649        // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
650        //
651        // Truncate res: abcdefgh… (truncate_at = 8)
652        // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
653        // 4, ... }, Run2 { string: …, len: 3, ... }
654        perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
655    }
656
657    #[test]
658    fn test_is_word_char() {
659        #[track_caller]
660        fn assert_word(word: &str) {
661            for c in word.chars() {
662                assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
663            }
664        }
665
666        #[track_caller]
667        fn assert_not_word(word: &str) {
668            let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
669            assert!(found, "assertion failed for '{}'", word);
670        }
671
672        assert_word("Hello123");
673        assert_word("non-English");
674        assert_word("var_name");
675        assert_word("123456");
676        assert_word("3.1415");
677        assert_word("10^2");
678        assert_word("1~2");
679        assert_word("100%");
680        assert_word("@mention");
681        assert_word("#hashtag");
682        assert_word("$variable");
683        assert_word("more⋯");
684        assert_word("won’t");
685        assert_word("‘twas");
686        assert_word("plz!");
687        assert_word("see)");
688        assert_word("quoted”");
689        assert_word("well…");
690        assert_word("foo\u{00A0}bar");
691        assert_word("foo\u{202F}bar");
692        assert_word("foo\u{2011}bar");
693
694        // Space
695        assert_not_word("foo bar");
696
697        // URL case
698        assert_word("https://github.com/zed-industries/zed/");
699        assert_word("github.com");
700        assert_word("a=1&b=2");
701
702        // Latin-1 Supplement
703        assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
704        // Latin Extended-A
705        assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
706        // Latin Extended-B
707        assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
708        // Cyrillic
709        assert_word("АБВГДЕЖЗИЙКЛМНОП");
710
711        // non-word characters
712        assert_not_word("你好");
713        assert_not_word("안녕하세요");
714        assert_not_word("こんにちは");
715        assert_not_word("😀😁😂");
716        assert_not_word("()[]{}<>");
717    }
718
719    // These seem to vary wildly based on the text system.
720    #[cfg(target_os = "macos")]
721    #[crate::test]
722    fn test_wrap_shaped_line(cx: &mut TestAppContext) {
723        cx.update(|cx| {
724            let text_system = WindowTextSystem::new(cx.text_system().clone());
725
726            let normal = TextRun {
727                len: 0,
728                font: font("Helvetica"),
729                color: Default::default(),
730                underline: Default::default(),
731                strikethrough: None,
732                background_color: None,
733            };
734            let bold = TextRun {
735                len: 0,
736                font: font("Helvetica").bold(),
737                color: Default::default(),
738                underline: Default::default(),
739                strikethrough: None,
740                background_color: None,
741            };
742
743            let text = "aa bbb cccc ddddd eeee".into();
744            let lines = text_system
745                .shape_text(
746                    text,
747                    px(16.),
748                    &[
749                        normal.with_len(4),
750                        bold.with_len(5),
751                        normal.with_len(6),
752                        bold.with_len(1),
753                        normal.with_len(7),
754                    ],
755                    Some(px(72.)),
756                    None,
757                )
758                .unwrap();
759
760            assert_eq!(
761                lines[0].layout.wrap_boundaries(),
762                &[
763                    WrapBoundary {
764                        run_ix: 0,
765                        glyph_ix: 7
766                    },
767                    WrapBoundary {
768                        run_ix: 0,
769                        glyph_ix: 12
770                    },
771                    WrapBoundary {
772                        run_ix: 0,
773                        glyph_ix: 18
774                    }
775                ],
776            );
777        });
778    }
779}