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