text_layout.rs

  1use crate::{
  2    color::Color,
  3    fonts::{FontId, GlyphId, Underline},
  4    geometry::{
  5        rect::RectF,
  6        vector::{vec2f, Vector2F},
  7    },
  8    platform,
  9    platform::FontSystem,
 10    scene,
 11    window::WindowContext,
 12    SceneBuilder,
 13};
 14use itertools::Itertools;
 15use ordered_float::OrderedFloat;
 16use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 17use smallvec::SmallVec;
 18use std::{
 19    borrow::Borrow,
 20    collections::HashMap,
 21    hash::{Hash, Hasher},
 22    iter,
 23    sync::Arc,
 24};
 25
 26pub struct TextLayoutCache {
 27    prev_frame: Mutex<HashMap<CacheKeyValue, Arc<LineLayout>>>,
 28    curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
 29    fonts: Arc<dyn platform::FontSystem>,
 30}
 31
 32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 33pub struct RunStyle {
 34    pub color: Color,
 35    pub font_id: FontId,
 36    pub underline: Underline,
 37}
 38
 39impl TextLayoutCache {
 40    pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
 41        Self {
 42            prev_frame: Mutex::new(HashMap::new()),
 43            curr_frame: RwLock::new(HashMap::new()),
 44            fonts,
 45        }
 46    }
 47
 48    pub fn finish_frame(&self) {
 49        let mut prev_frame = self.prev_frame.lock();
 50        let mut curr_frame = self.curr_frame.write();
 51        std::mem::swap(&mut *prev_frame, &mut *curr_frame);
 52        curr_frame.clear();
 53    }
 54
 55    pub fn layout_str<'a>(
 56        &'a self,
 57        text: &'a str,
 58        font_size: f32,
 59        runs: &'a [(usize, RunStyle)],
 60    ) -> Line {
 61        let key = &CacheKeyRef {
 62            text,
 63            font_size: OrderedFloat(font_size),
 64            runs,
 65        } as &dyn CacheKey;
 66        let curr_frame = self.curr_frame.upgradable_read();
 67        if let Some(layout) = curr_frame.get(key) {
 68            return Line::new(layout.clone(), runs);
 69        }
 70
 71        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
 72        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
 73            curr_frame.insert(key, layout.clone());
 74            Line::new(layout, runs)
 75        } else {
 76            let layout = Arc::new(self.fonts.layout_line(text, font_size, runs));
 77            let key = CacheKeyValue {
 78                text: text.into(),
 79                font_size: OrderedFloat(font_size),
 80                runs: SmallVec::from(runs),
 81            };
 82            curr_frame.insert(key, layout.clone());
 83            Line::new(layout, runs)
 84        }
 85    }
 86}
 87
 88trait CacheKey {
 89    fn key(&self) -> CacheKeyRef;
 90}
 91
 92impl<'a> PartialEq for (dyn CacheKey + 'a) {
 93    fn eq(&self, other: &dyn CacheKey) -> bool {
 94        self.key() == other.key()
 95    }
 96}
 97
 98impl<'a> Eq for (dyn CacheKey + 'a) {}
 99
100impl<'a> Hash for (dyn CacheKey + 'a) {
101    fn hash<H: Hasher>(&self, state: &mut H) {
102        self.key().hash(state)
103    }
104}
105
106#[derive(Eq)]
107struct CacheKeyValue {
108    text: String,
109    font_size: OrderedFloat<f32>,
110    runs: SmallVec<[(usize, RunStyle); 1]>,
111}
112
113impl CacheKey for CacheKeyValue {
114    fn key(&self) -> CacheKeyRef {
115        CacheKeyRef {
116            text: self.text.as_str(),
117            font_size: self.font_size,
118            runs: self.runs.as_slice(),
119        }
120    }
121}
122
123impl PartialEq for CacheKeyValue {
124    fn eq(&self, other: &Self) -> bool {
125        self.key().eq(&other.key())
126    }
127}
128
129impl Hash for CacheKeyValue {
130    fn hash<H: Hasher>(&self, state: &mut H) {
131        self.key().hash(state);
132    }
133}
134
135impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
136    fn borrow(&self) -> &(dyn CacheKey + 'a) {
137        self as &dyn CacheKey
138    }
139}
140
141#[derive(Copy, Clone)]
142struct CacheKeyRef<'a> {
143    text: &'a str,
144    font_size: OrderedFloat<f32>,
145    runs: &'a [(usize, RunStyle)],
146}
147
148impl<'a> CacheKey for CacheKeyRef<'a> {
149    fn key(&self) -> CacheKeyRef {
150        *self
151    }
152}
153
154impl<'a> PartialEq for CacheKeyRef<'a> {
155    fn eq(&self, other: &Self) -> bool {
156        self.text == other.text
157            && self.font_size == other.font_size
158            && self.runs.len() == other.runs.len()
159            && self.runs.iter().zip(other.runs.iter()).all(
160                |((len_a, style_a), (len_b, style_b))| {
161                    len_a == len_b && style_a.font_id == style_b.font_id
162                },
163            )
164    }
165}
166
167impl<'a> Hash for CacheKeyRef<'a> {
168    fn hash<H: Hasher>(&self, state: &mut H) {
169        self.text.hash(state);
170        self.font_size.hash(state);
171        for (len, style_id) in self.runs {
172            len.hash(state);
173            style_id.font_id.hash(state);
174        }
175    }
176}
177
178#[derive(Default, Debug, Clone)]
179pub struct Line {
180    layout: Arc<LineLayout>,
181    style_runs: SmallVec<[StyleRun; 32]>,
182    pub invisibles: SmallVec<[Invisible; 32]>,
183}
184
185#[derive(Debug, Clone, Copy)]
186struct StyleRun {
187    len: u32,
188    color: Color,
189    underline: Underline,
190}
191
192#[derive(Default, Debug)]
193pub struct LineLayout {
194    pub width: f32,
195    pub ascent: f32,
196    pub descent: f32,
197    pub runs: Vec<Run>,
198    pub len: usize,
199    pub font_size: f32,
200}
201
202#[derive(Debug)]
203pub struct Run {
204    pub font_id: FontId,
205    pub glyphs: Vec<Glyph>,
206}
207
208#[derive(Clone, Debug)]
209pub struct Glyph {
210    pub id: GlyphId,
211    pub position: Vector2F,
212    pub index: usize,
213    pub is_emoji: bool,
214}
215
216#[derive(Debug, Clone)]
217pub enum Invisible {
218    Tab { range: std::ops::Range<usize> },
219    Whitespace { range: std::ops::Range<usize> },
220}
221
222impl Line {
223    fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
224        let mut style_runs = SmallVec::new();
225        for (len, style) in runs {
226            style_runs.push(StyleRun {
227                len: *len as u32,
228                color: style.color,
229                underline: style.underline,
230            });
231        }
232        Self {
233            layout,
234            style_runs,
235            invisibles: SmallVec::new(),
236        }
237    }
238
239    pub fn runs(&self) -> &[Run] {
240        &self.layout.runs
241    }
242
243    pub fn width(&self) -> f32 {
244        self.layout.width
245    }
246
247    pub fn font_size(&self) -> f32 {
248        self.layout.font_size
249    }
250
251    pub fn x_for_index(&self, index: usize) -> f32 {
252        for run in &self.layout.runs {
253            for glyph in &run.glyphs {
254                if glyph.index >= index {
255                    return glyph.position.x();
256                }
257            }
258        }
259        self.layout.width
260    }
261
262    pub fn font_for_index(&self, index: usize) -> Option<FontId> {
263        for run in &self.layout.runs {
264            for glyph in &run.glyphs {
265                if glyph.index >= index {
266                    return Some(run.font_id);
267                }
268            }
269        }
270
271        None
272    }
273
274    pub fn len(&self) -> usize {
275        self.layout.len
276    }
277
278    pub fn is_empty(&self) -> bool {
279        self.layout.len == 0
280    }
281
282    pub fn index_for_x(&self, x: f32) -> Option<usize> {
283        if x >= self.layout.width {
284            None
285        } else {
286            for run in self.layout.runs.iter().rev() {
287                for glyph in run.glyphs.iter().rev() {
288                    if glyph.position.x() <= x {
289                        return Some(glyph.index);
290                    }
291                }
292            }
293            Some(0)
294        }
295    }
296
297    pub fn paint(
298        &self,
299        scene: &mut SceneBuilder,
300        origin: Vector2F,
301        visible_bounds: RectF,
302        line_height: f32,
303        cx: &mut WindowContext,
304    ) {
305        let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
306        let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
307
308        let mut style_runs = self.style_runs.iter();
309        let mut run_end = 0;
310        let mut color = Color::black();
311        let mut underline = None;
312
313        let tab_ranges = self
314            .invisibles
315            .iter()
316            .filter_map(|invisible| match invisible {
317                Invisible::Tab { range } => Some(range),
318                Invisible::Whitespace { .. } => None,
319            })
320            .sorted_by(|tab_range_1, tab_range_2| tab_range_1.start.cmp(&tab_range_2.start))
321            .collect::<Vec<_>>();
322
323        for run in &self.layout.runs {
324            let max_glyph_width = cx
325                .font_cache
326                .bounding_box(run.font_id, self.layout.font_size)
327                .x();
328
329            for glyph in &run.glyphs {
330                let glyph_origin = origin + baseline_offset + glyph.position;
331                if glyph_origin.x() > visible_bounds.upper_right().x() {
332                    break;
333                }
334
335                let mut finished_underline = None;
336                if glyph.index >= run_end {
337                    if let Some(style_run) = style_runs.next() {
338                        if let Some((_, underline_style)) = underline {
339                            if style_run.underline != underline_style {
340                                finished_underline = underline.take();
341                            }
342                        }
343                        if style_run.underline.thickness.into_inner() > 0. {
344                            underline.get_or_insert((
345                                vec2f(
346                                    glyph_origin.x(),
347                                    origin.y() + baseline_offset.y() + 0.618 * self.layout.descent,
348                                ),
349                                Underline {
350                                    color: Some(
351                                        style_run.underline.color.unwrap_or(style_run.color),
352                                    ),
353                                    thickness: style_run.underline.thickness,
354                                    squiggly: style_run.underline.squiggly,
355                                },
356                            ));
357                        }
358
359                        run_end += style_run.len as usize;
360                        color = style_run.color;
361                    } else {
362                        run_end = self.layout.len;
363                        finished_underline = underline.take();
364                    }
365                }
366
367                if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
368                    continue;
369                }
370
371                if let Some((underline_origin, underline_style)) = finished_underline {
372                    scene.push_underline(scene::Underline {
373                        origin: underline_origin,
374                        width: glyph_origin.x() - underline_origin.x(),
375                        thickness: underline_style.thickness.into(),
376                        color: underline_style.color.unwrap(),
377                        squiggly: underline_style.squiggly,
378                    });
379                }
380
381                if glyph.is_emoji {
382                    scene.push_image_glyph(scene::ImageGlyph {
383                        font_id: run.font_id,
384                        font_size: self.layout.font_size,
385                        id: glyph.id,
386                        origin: glyph_origin,
387                    });
388                } else {
389                    let id = if tab_ranges.iter().any(|tab_range| {
390                        tab_range.start <= glyph.index && glyph.index < tab_range.end
391                    }) {
392                        // TODO kb get a proper (cached) glyph
393                        glyph.id + 100
394                    } else {
395                        glyph.id
396                    };
397
398                    scene.push_glyph(scene::Glyph {
399                        font_id: run.font_id,
400                        font_size: self.layout.font_size,
401                        id,
402                        origin: glyph_origin,
403                        color,
404                    });
405                }
406            }
407        }
408
409        if let Some((underline_start, underline_style)) = underline.take() {
410            let line_end_x = origin.x() + self.layout.width;
411            scene.push_underline(scene::Underline {
412                origin: underline_start,
413                width: line_end_x - underline_start.x(),
414                color: underline_style.color.unwrap(),
415                thickness: underline_style.thickness.into(),
416                squiggly: underline_style.squiggly,
417            });
418        }
419    }
420
421    pub fn paint_wrapped(
422        &self,
423        scene: &mut SceneBuilder,
424        origin: Vector2F,
425        visible_bounds: RectF,
426        line_height: f32,
427        boundaries: &[ShapedBoundary],
428        cx: &mut WindowContext,
429    ) {
430        let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
431        let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
432
433        let mut boundaries = boundaries.into_iter().peekable();
434        let mut color_runs = self.style_runs.iter();
435        let mut style_run_end = 0;
436        let mut color = Color::black();
437        let mut underline: Option<(Vector2F, Underline)> = None;
438
439        let mut glyph_origin = origin;
440        let mut prev_position = 0.;
441        for (run_ix, run) in self.layout.runs.iter().enumerate() {
442            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
443                glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
444
445                if boundaries
446                    .peek()
447                    .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
448                {
449                    boundaries.next();
450                    if let Some((underline_origin, underline_style)) = underline {
451                        scene.push_underline(scene::Underline {
452                            origin: underline_origin,
453                            width: glyph_origin.x() - underline_origin.x(),
454                            thickness: underline_style.thickness.into(),
455                            color: underline_style.color.unwrap(),
456                            squiggly: underline_style.squiggly,
457                        });
458                    }
459
460                    glyph_origin = vec2f(origin.x(), glyph_origin.y() + line_height);
461                }
462                prev_position = glyph.position.x();
463
464                let mut finished_underline = None;
465                if glyph.index >= style_run_end {
466                    if let Some(style_run) = color_runs.next() {
467                        style_run_end += style_run.len as usize;
468                        color = style_run.color;
469                        if let Some((_, underline_style)) = underline {
470                            if style_run.underline != underline_style {
471                                finished_underline = underline.take();
472                            }
473                        }
474                        if style_run.underline.thickness.into_inner() > 0. {
475                            underline.get_or_insert((
476                                glyph_origin
477                                    + vec2f(0., baseline_offset.y() + 0.618 * self.layout.descent),
478                                Underline {
479                                    color: Some(
480                                        style_run.underline.color.unwrap_or(style_run.color),
481                                    ),
482                                    thickness: style_run.underline.thickness,
483                                    squiggly: style_run.underline.squiggly,
484                                },
485                            ));
486                        }
487                    } else {
488                        style_run_end = self.layout.len;
489                        color = Color::black();
490                        finished_underline = underline.take();
491                    }
492                }
493
494                if let Some((underline_origin, underline_style)) = finished_underline {
495                    scene.push_underline(scene::Underline {
496                        origin: underline_origin,
497                        width: glyph_origin.x() - underline_origin.x(),
498                        thickness: underline_style.thickness.into(),
499                        color: underline_style.color.unwrap(),
500                        squiggly: underline_style.squiggly,
501                    });
502                }
503
504                let glyph_bounds = RectF::new(
505                    glyph_origin,
506                    cx.font_cache
507                        .bounding_box(run.font_id, self.layout.font_size),
508                );
509                if glyph_bounds.intersects(visible_bounds) {
510                    if glyph.is_emoji {
511                        scene.push_image_glyph(scene::ImageGlyph {
512                            font_id: run.font_id,
513                            font_size: self.layout.font_size,
514                            id: glyph.id,
515                            origin: glyph_bounds.origin() + baseline_offset,
516                        });
517                    } else {
518                        scene.push_glyph(scene::Glyph {
519                            font_id: run.font_id,
520                            font_size: self.layout.font_size,
521                            id: glyph.id,
522                            origin: glyph_bounds.origin() + baseline_offset,
523                            color,
524                        });
525                    }
526                }
527            }
528        }
529
530        if let Some((underline_origin, underline_style)) = underline.take() {
531            let line_end_x = glyph_origin.x() + self.layout.width - prev_position;
532            scene.push_underline(scene::Underline {
533                origin: underline_origin,
534                width: line_end_x - underline_origin.x(),
535                thickness: underline_style.thickness.into(),
536                color: underline_style.color.unwrap(),
537                squiggly: underline_style.squiggly,
538            });
539        }
540    }
541}
542
543impl Run {
544    pub fn glyphs(&self) -> &[Glyph] {
545        &self.glyphs
546    }
547}
548
549#[derive(Copy, Clone, Debug, PartialEq, Eq)]
550pub struct Boundary {
551    pub ix: usize,
552    pub next_indent: u32,
553}
554
555#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
556pub struct ShapedBoundary {
557    pub run_ix: usize,
558    pub glyph_ix: usize,
559}
560
561impl Boundary {
562    fn new(ix: usize, next_indent: u32) -> Self {
563        Self { ix, next_indent }
564    }
565}
566
567pub struct LineWrapper {
568    font_system: Arc<dyn FontSystem>,
569    pub(crate) font_id: FontId,
570    pub(crate) font_size: f32,
571    cached_ascii_char_widths: [f32; 128],
572    cached_other_char_widths: HashMap<char, f32>,
573}
574
575impl LineWrapper {
576    pub const MAX_INDENT: u32 = 256;
577
578    pub fn new(font_id: FontId, font_size: f32, font_system: Arc<dyn FontSystem>) -> Self {
579        Self {
580            font_system,
581            font_id,
582            font_size,
583            cached_ascii_char_widths: [f32::NAN; 128],
584            cached_other_char_widths: HashMap::new(),
585        }
586    }
587
588    pub fn wrap_line<'a>(
589        &'a mut self,
590        line: &'a str,
591        wrap_width: f32,
592    ) -> impl Iterator<Item = Boundary> + 'a {
593        let mut width = 0.0;
594        let mut first_non_whitespace_ix = None;
595        let mut indent = None;
596        let mut last_candidate_ix = 0;
597        let mut last_candidate_width = 0.0;
598        let mut last_wrap_ix = 0;
599        let mut prev_c = '\0';
600        let mut char_indices = line.char_indices();
601        iter::from_fn(move || {
602            for (ix, c) in char_indices.by_ref() {
603                if c == '\n' {
604                    continue;
605                }
606
607                if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
608                    last_candidate_ix = ix;
609                    last_candidate_width = width;
610                }
611
612                if c != ' ' && first_non_whitespace_ix.is_none() {
613                    first_non_whitespace_ix = Some(ix);
614                }
615
616                let char_width = self.width_for_char(c);
617                width += char_width;
618                if width > wrap_width && ix > last_wrap_ix {
619                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
620                    {
621                        indent = Some(
622                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
623                        );
624                    }
625
626                    if last_candidate_ix > 0 {
627                        last_wrap_ix = last_candidate_ix;
628                        width -= last_candidate_width;
629                        last_candidate_ix = 0;
630                    } else {
631                        last_wrap_ix = ix;
632                        width = char_width;
633                    }
634
635                    let indent_width =
636                        indent.map(|indent| indent as f32 * self.width_for_char(' '));
637                    width += indent_width.unwrap_or(0.);
638
639                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
640                }
641                prev_c = c;
642            }
643
644            None
645        })
646    }
647
648    pub fn wrap_shaped_line<'a>(
649        &'a mut self,
650        str: &'a str,
651        line: &'a Line,
652        wrap_width: f32,
653    ) -> impl Iterator<Item = ShapedBoundary> + 'a {
654        let mut first_non_whitespace_ix = None;
655        let mut last_candidate_ix = None;
656        let mut last_candidate_x = 0.0;
657        let mut last_wrap_ix = ShapedBoundary {
658            run_ix: 0,
659            glyph_ix: 0,
660        };
661        let mut last_wrap_x = 0.;
662        let mut prev_c = '\0';
663        let mut glyphs = line
664            .runs()
665            .iter()
666            .enumerate()
667            .flat_map(move |(run_ix, run)| {
668                run.glyphs()
669                    .iter()
670                    .enumerate()
671                    .map(move |(glyph_ix, glyph)| {
672                        let character = str[glyph.index..].chars().next().unwrap();
673                        (
674                            ShapedBoundary { run_ix, glyph_ix },
675                            character,
676                            glyph.position.x(),
677                        )
678                    })
679            })
680            .peekable();
681
682        iter::from_fn(move || {
683            while let Some((ix, c, x)) = glyphs.next() {
684                if c == '\n' {
685                    continue;
686                }
687
688                if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
689                    last_candidate_ix = Some(ix);
690                    last_candidate_x = x;
691                }
692
693                if c != ' ' && first_non_whitespace_ix.is_none() {
694                    first_non_whitespace_ix = Some(ix);
695                }
696
697                let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
698                let width = next_x - last_wrap_x;
699                if width > wrap_width && ix > last_wrap_ix {
700                    if let Some(last_candidate_ix) = last_candidate_ix.take() {
701                        last_wrap_ix = last_candidate_ix;
702                        last_wrap_x = last_candidate_x;
703                    } else {
704                        last_wrap_ix = ix;
705                        last_wrap_x = x;
706                    }
707
708                    return Some(last_wrap_ix);
709                }
710                prev_c = c;
711            }
712
713            None
714        })
715    }
716
717    fn is_boundary(&self, prev: char, next: char) -> bool {
718        (prev == ' ') && (next != ' ')
719    }
720
721    #[inline(always)]
722    fn width_for_char(&mut self, c: char) -> f32 {
723        if (c as u32) < 128 {
724            let mut width = self.cached_ascii_char_widths[c as usize];
725            if width.is_nan() {
726                width = self.compute_width_for_char(c);
727                self.cached_ascii_char_widths[c as usize] = width;
728            }
729            width
730        } else {
731            let mut width = self
732                .cached_other_char_widths
733                .get(&c)
734                .copied()
735                .unwrap_or(f32::NAN);
736            if width.is_nan() {
737                width = self.compute_width_for_char(c);
738                self.cached_other_char_widths.insert(c, width);
739            }
740            width
741        }
742    }
743
744    fn compute_width_for_char(&self, c: char) -> f32 {
745        self.font_system
746            .layout_line(
747                &c.to_string(),
748                self.font_size,
749                &[(
750                    1,
751                    RunStyle {
752                        font_id: self.font_id,
753                        color: Default::default(),
754                        underline: Default::default(),
755                    },
756                )],
757            )
758            .width
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::fonts::{Properties, Weight};
766
767    #[crate::test(self)]
768    fn test_wrap_line(cx: &mut crate::AppContext) {
769        let font_cache = cx.font_cache().clone();
770        let font_system = cx.platform().fonts();
771        let family = font_cache
772            .load_family(&["Courier"], &Default::default())
773            .unwrap();
774        let font_id = font_cache.select_font(family, &Default::default()).unwrap();
775
776        let mut wrapper = LineWrapper::new(font_id, 16., font_system);
777        assert_eq!(
778            wrapper
779                .wrap_line("aa bbb cccc ddddd eeee", 72.0)
780                .collect::<Vec<_>>(),
781            &[
782                Boundary::new(7, 0),
783                Boundary::new(12, 0),
784                Boundary::new(18, 0)
785            ],
786        );
787        assert_eq!(
788            wrapper
789                .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
790                .collect::<Vec<_>>(),
791            &[
792                Boundary::new(4, 0),
793                Boundary::new(11, 0),
794                Boundary::new(18, 0)
795            ],
796        );
797        assert_eq!(
798            wrapper.wrap_line("     aaaaaaa", 72.).collect::<Vec<_>>(),
799            &[
800                Boundary::new(7, 5),
801                Boundary::new(9, 5),
802                Boundary::new(11, 5),
803            ]
804        );
805        assert_eq!(
806            wrapper
807                .wrap_line("                            ", 72.)
808                .collect::<Vec<_>>(),
809            &[
810                Boundary::new(7, 0),
811                Boundary::new(14, 0),
812                Boundary::new(21, 0)
813            ]
814        );
815        assert_eq!(
816            wrapper
817                .wrap_line("          aaaaaaaaaaaaaa", 72.)
818                .collect::<Vec<_>>(),
819            &[
820                Boundary::new(7, 0),
821                Boundary::new(14, 3),
822                Boundary::new(18, 3),
823                Boundary::new(22, 3),
824            ]
825        );
826    }
827
828    #[crate::test(self, retries = 5)]
829    fn test_wrap_shaped_line(cx: &mut crate::AppContext) {
830        // This is failing intermittently on CI and we don't have time to figure it out
831        let font_cache = cx.font_cache().clone();
832        let font_system = cx.platform().fonts();
833        let text_layout_cache = TextLayoutCache::new(font_system.clone());
834
835        let family = font_cache
836            .load_family(&["Helvetica"], &Default::default())
837            .unwrap();
838        let font_id = font_cache.select_font(family, &Default::default()).unwrap();
839        let normal = RunStyle {
840            font_id,
841            color: Default::default(),
842            underline: Default::default(),
843        };
844        let bold = RunStyle {
845            font_id: font_cache
846                .select_font(
847                    family,
848                    &Properties {
849                        weight: Weight::BOLD,
850                        ..Default::default()
851                    },
852                )
853                .unwrap(),
854            color: Default::default(),
855            underline: Default::default(),
856        };
857
858        let text = "aa bbb cccc ddddd eeee";
859        let line = text_layout_cache.layout_str(
860            text,
861            16.0,
862            &[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)],
863        );
864
865        let mut wrapper = LineWrapper::new(font_id, 16., font_system);
866        assert_eq!(
867            wrapper
868                .wrap_shaped_line(text, &line, 72.0)
869                .collect::<Vec<_>>(),
870            &[
871                ShapedBoundary {
872                    run_ix: 1,
873                    glyph_ix: 3
874                },
875                ShapedBoundary {
876                    run_ix: 2,
877                    glyph_ix: 3
878                },
879                ShapedBoundary {
880                    run_ix: 4,
881                    glyph_ix: 2
882                }
883            ],
884        );
885    }
886}