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