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    pub fn index_for_x(&self, x: f32) -> Option<usize> {
270        if x >= self.layout.width {
271            None
272        } else {
273            for run in self.layout.runs.iter().rev() {
274                for glyph in run.glyphs.iter().rev() {
275                    if glyph.position.x() <= x {
276                        return Some(glyph.index);
277                    }
278                }
279            }
280            Some(0)
281        }
282    }
283
284    pub fn paint(
285        &self,
286        origin: Vector2F,
287        visible_bounds: RectF,
288        line_height: f32,
289        cx: &mut WindowContext,
290    ) {
291        let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
292        let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
293
294        let mut style_runs = self.style_runs.iter();
295        let mut run_end = 0;
296        let mut color = Color::black();
297        let mut underline = None;
298
299        for run in &self.layout.runs {
300            let max_glyph_width = cx
301                .font_cache
302                .bounding_box(run.font_id, self.layout.font_size)
303                .x();
304
305            for glyph in &run.glyphs {
306                let glyph_origin = origin + baseline_offset + glyph.position;
307                if glyph_origin.x() > visible_bounds.upper_right().x() {
308                    break;
309                }
310
311                let mut finished_underline = None;
312                if glyph.index >= run_end {
313                    if let Some(style_run) = style_runs.next() {
314                        if let Some((_, underline_style)) = underline {
315                            if style_run.underline != underline_style {
316                                finished_underline = underline.take();
317                            }
318                        }
319                        if style_run.underline.thickness.into_inner() > 0. {
320                            underline.get_or_insert((
321                                vec2f(
322                                    glyph_origin.x(),
323                                    origin.y() + baseline_offset.y() + 0.618 * self.layout.descent,
324                                ),
325                                Underline {
326                                    color: Some(
327                                        style_run.underline.color.unwrap_or(style_run.color),
328                                    ),
329                                    thickness: style_run.underline.thickness,
330                                    squiggly: style_run.underline.squiggly,
331                                },
332                            ));
333                        }
334
335                        run_end += style_run.len as usize;
336                        color = style_run.color;
337                    } else {
338                        run_end = self.layout.len;
339                        finished_underline = underline.take();
340                    }
341                }
342
343                if glyph_origin.x() + max_glyph_width < visible_bounds.origin().x() {
344                    continue;
345                }
346
347                if let Some((underline_origin, underline_style)) = finished_underline {
348                    cx.scene().push_underline(scene::Underline {
349                        origin: underline_origin,
350                        width: glyph_origin.x() - underline_origin.x(),
351                        thickness: underline_style.thickness.into(),
352                        color: underline_style.color.unwrap(),
353                        squiggly: underline_style.squiggly,
354                    });
355                }
356
357                if glyph.is_emoji {
358                    cx.scene().push_image_glyph(scene::ImageGlyph {
359                        font_id: run.font_id,
360                        font_size: self.layout.font_size,
361                        id: glyph.id,
362                        origin: glyph_origin,
363                    });
364                } else {
365                    cx.scene().push_glyph(scene::Glyph {
366                        font_id: run.font_id,
367                        font_size: self.layout.font_size,
368                        id: glyph.id,
369                        origin: glyph_origin,
370                        color,
371                    });
372                }
373            }
374        }
375
376        if let Some((underline_start, underline_style)) = underline.take() {
377            let line_end_x = origin.x() + self.layout.width;
378            cx.scene().push_underline(scene::Underline {
379                origin: underline_start,
380                width: line_end_x - underline_start.x(),
381                color: underline_style.color.unwrap(),
382                thickness: underline_style.thickness.into(),
383                squiggly: underline_style.squiggly,
384            });
385        }
386    }
387
388    pub fn paint_wrapped(
389        &self,
390        origin: Vector2F,
391        visible_bounds: RectF,
392        line_height: f32,
393        boundaries: &[ShapedBoundary],
394        cx: &mut WindowContext,
395    ) {
396        let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
397        let baseline_offset = vec2f(0., padding_top + self.layout.ascent);
398
399        let mut boundaries = boundaries.into_iter().peekable();
400        let mut color_runs = self.style_runs.iter();
401        let mut style_run_end = 0;
402        let mut color = Color::black();
403        let mut underline: Option<(Vector2F, Underline)> = None;
404
405        let mut glyph_origin = origin;
406        let mut prev_position = 0.;
407        for (run_ix, run) in self.layout.runs.iter().enumerate() {
408            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
409                glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
410
411                if boundaries
412                    .peek()
413                    .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
414                {
415                    boundaries.next();
416                    if let Some((underline_origin, underline_style)) = underline {
417                        cx.scene().push_underline(scene::Underline {
418                            origin: underline_origin,
419                            width: glyph_origin.x() - underline_origin.x(),
420                            thickness: underline_style.thickness.into(),
421                            color: underline_style.color.unwrap(),
422                            squiggly: underline_style.squiggly,
423                        });
424                    }
425
426                    glyph_origin = vec2f(origin.x(), glyph_origin.y() + line_height);
427                }
428                prev_position = glyph.position.x();
429
430                let mut finished_underline = None;
431                if glyph.index >= style_run_end {
432                    if let Some(style_run) = color_runs.next() {
433                        style_run_end += style_run.len as usize;
434                        color = style_run.color;
435                        if let Some((_, underline_style)) = underline {
436                            if style_run.underline != underline_style {
437                                finished_underline = underline.take();
438                            }
439                        }
440                        if style_run.underline.thickness.into_inner() > 0. {
441                            underline.get_or_insert((
442                                glyph_origin
443                                    + vec2f(0., baseline_offset.y() + 0.618 * self.layout.descent),
444                                Underline {
445                                    color: Some(
446                                        style_run.underline.color.unwrap_or(style_run.color),
447                                    ),
448                                    thickness: style_run.underline.thickness,
449                                    squiggly: style_run.underline.squiggly,
450                                },
451                            ));
452                        }
453                    } else {
454                        style_run_end = self.layout.len;
455                        color = Color::black();
456                        finished_underline = underline.take();
457                    }
458                }
459
460                if let Some((underline_origin, underline_style)) = finished_underline {
461                    cx.scene().push_underline(scene::Underline {
462                        origin: underline_origin,
463                        width: glyph_origin.x() - underline_origin.x(),
464                        thickness: underline_style.thickness.into(),
465                        color: underline_style.color.unwrap(),
466                        squiggly: underline_style.squiggly,
467                    });
468                }
469
470                let glyph_bounds = RectF::new(
471                    glyph_origin,
472                    cx.font_cache
473                        .bounding_box(run.font_id, self.layout.font_size),
474                );
475                if glyph_bounds.intersects(visible_bounds) {
476                    if glyph.is_emoji {
477                        cx.scene().push_image_glyph(scene::ImageGlyph {
478                            font_id: run.font_id,
479                            font_size: self.layout.font_size,
480                            id: glyph.id,
481                            origin: glyph_bounds.origin() + baseline_offset,
482                        });
483                    } else {
484                        cx.scene().push_glyph(scene::Glyph {
485                            font_id: run.font_id,
486                            font_size: self.layout.font_size,
487                            id: glyph.id,
488                            origin: glyph_bounds.origin() + baseline_offset,
489                            color,
490                        });
491                    }
492                }
493            }
494        }
495
496        if let Some((underline_origin, underline_style)) = underline.take() {
497            let line_end_x = glyph_origin.x() + self.layout.width - prev_position;
498            cx.scene().push_underline(scene::Underline {
499                origin: underline_origin,
500                width: line_end_x - underline_origin.x(),
501                thickness: underline_style.thickness.into(),
502                color: underline_style.color.unwrap(),
503                squiggly: underline_style.squiggly,
504            });
505        }
506    }
507}
508
509impl Run {
510    pub fn glyphs(&self) -> &[Glyph] {
511        &self.glyphs
512    }
513}
514
515#[derive(Copy, Clone, Debug, PartialEq, Eq)]
516pub struct Boundary {
517    pub ix: usize,
518    pub next_indent: u32,
519}
520
521#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
522pub struct ShapedBoundary {
523    pub run_ix: usize,
524    pub glyph_ix: usize,
525}
526
527impl Boundary {
528    fn new(ix: usize, next_indent: u32) -> Self {
529        Self { ix, next_indent }
530    }
531}
532
533pub struct LineWrapper {
534    font_system: Arc<dyn FontSystem>,
535    pub(crate) font_id: FontId,
536    pub(crate) font_size: f32,
537    cached_ascii_char_widths: [f32; 128],
538    cached_other_char_widths: HashMap<char, f32>,
539}
540
541impl LineWrapper {
542    pub const MAX_INDENT: u32 = 256;
543
544    pub fn new(font_id: FontId, font_size: f32, font_system: Arc<dyn FontSystem>) -> Self {
545        Self {
546            font_system,
547            font_id,
548            font_size,
549            cached_ascii_char_widths: [f32::NAN; 128],
550            cached_other_char_widths: HashMap::new(),
551        }
552    }
553
554    pub fn wrap_line<'a>(
555        &'a mut self,
556        line: &'a str,
557        wrap_width: f32,
558    ) -> impl Iterator<Item = Boundary> + 'a {
559        let mut width = 0.0;
560        let mut first_non_whitespace_ix = None;
561        let mut indent = None;
562        let mut last_candidate_ix = 0;
563        let mut last_candidate_width = 0.0;
564        let mut last_wrap_ix = 0;
565        let mut prev_c = '\0';
566        let mut char_indices = line.char_indices();
567        iter::from_fn(move || {
568            for (ix, c) in char_indices.by_ref() {
569                if c == '\n' {
570                    continue;
571                }
572
573                if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
574                    last_candidate_ix = ix;
575                    last_candidate_width = width;
576                }
577
578                if c != ' ' && first_non_whitespace_ix.is_none() {
579                    first_non_whitespace_ix = Some(ix);
580                }
581
582                let char_width = self.width_for_char(c);
583                width += char_width;
584                if width > wrap_width && ix > last_wrap_ix {
585                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
586                    {
587                        indent = Some(
588                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
589                        );
590                    }
591
592                    if last_candidate_ix > 0 {
593                        last_wrap_ix = last_candidate_ix;
594                        width -= last_candidate_width;
595                        last_candidate_ix = 0;
596                    } else {
597                        last_wrap_ix = ix;
598                        width = char_width;
599                    }
600
601                    let indent_width =
602                        indent.map(|indent| indent as f32 * self.width_for_char(' '));
603                    width += indent_width.unwrap_or(0.);
604
605                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
606                }
607                prev_c = c;
608            }
609
610            None
611        })
612    }
613
614    pub fn wrap_shaped_line<'a>(
615        &'a mut self,
616        str: &'a str,
617        line: &'a Line,
618        wrap_width: f32,
619    ) -> impl Iterator<Item = ShapedBoundary> + 'a {
620        let mut first_non_whitespace_ix = None;
621        let mut last_candidate_ix = None;
622        let mut last_candidate_x = 0.0;
623        let mut last_wrap_ix = ShapedBoundary {
624            run_ix: 0,
625            glyph_ix: 0,
626        };
627        let mut last_wrap_x = 0.;
628        let mut prev_c = '\0';
629        let mut glyphs = line
630            .runs()
631            .iter()
632            .enumerate()
633            .flat_map(move |(run_ix, run)| {
634                run.glyphs()
635                    .iter()
636                    .enumerate()
637                    .map(move |(glyph_ix, glyph)| {
638                        let character = str[glyph.index..].chars().next().unwrap();
639                        (
640                            ShapedBoundary { run_ix, glyph_ix },
641                            character,
642                            glyph.position.x(),
643                        )
644                    })
645            })
646            .peekable();
647
648        iter::from_fn(move || {
649            while let Some((ix, c, x)) = glyphs.next() {
650                if c == '\n' {
651                    continue;
652                }
653
654                if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
655                    last_candidate_ix = Some(ix);
656                    last_candidate_x = x;
657                }
658
659                if c != ' ' && first_non_whitespace_ix.is_none() {
660                    first_non_whitespace_ix = Some(ix);
661                }
662
663                let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
664                let width = next_x - last_wrap_x;
665                if width > wrap_width && ix > last_wrap_ix {
666                    if let Some(last_candidate_ix) = last_candidate_ix.take() {
667                        last_wrap_ix = last_candidate_ix;
668                        last_wrap_x = last_candidate_x;
669                    } else {
670                        last_wrap_ix = ix;
671                        last_wrap_x = x;
672                    }
673
674                    return Some(last_wrap_ix);
675                }
676                prev_c = c;
677            }
678
679            None
680        })
681    }
682
683    fn is_boundary(&self, prev: char, next: char) -> bool {
684        (prev == ' ') && (next != ' ')
685    }
686
687    #[inline(always)]
688    fn width_for_char(&mut self, c: char) -> f32 {
689        if (c as u32) < 128 {
690            let mut width = self.cached_ascii_char_widths[c as usize];
691            if width.is_nan() {
692                width = self.compute_width_for_char(c);
693                self.cached_ascii_char_widths[c as usize] = width;
694            }
695            width
696        } else {
697            let mut width = self
698                .cached_other_char_widths
699                .get(&c)
700                .copied()
701                .unwrap_or(f32::NAN);
702            if width.is_nan() {
703                width = self.compute_width_for_char(c);
704                self.cached_other_char_widths.insert(c, width);
705            }
706            width
707        }
708    }
709
710    fn compute_width_for_char(&self, c: char) -> f32 {
711        self.font_system
712            .layout_line(
713                &c.to_string(),
714                self.font_size,
715                &[(
716                    1,
717                    RunStyle {
718                        font_id: self.font_id,
719                        color: Default::default(),
720                        underline: Default::default(),
721                    },
722                )],
723            )
724            .width
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use crate::fonts::{Properties, Weight};
732
733    #[crate::test(self)]
734    fn test_wrap_line(cx: &mut crate::AppContext) {
735        let font_cache = cx.font_cache().clone();
736        let font_system = cx.platform().fonts();
737        let family = font_cache
738            .load_family(&["Courier"], &Default::default())
739            .unwrap();
740        let font_id = font_cache.select_font(family, &Default::default()).unwrap();
741
742        let mut wrapper = LineWrapper::new(font_id, 16., font_system);
743        assert_eq!(
744            wrapper
745                .wrap_line("aa bbb cccc ddddd eeee", 72.0)
746                .collect::<Vec<_>>(),
747            &[
748                Boundary::new(7, 0),
749                Boundary::new(12, 0),
750                Boundary::new(18, 0)
751            ],
752        );
753        assert_eq!(
754            wrapper
755                .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
756                .collect::<Vec<_>>(),
757            &[
758                Boundary::new(4, 0),
759                Boundary::new(11, 0),
760                Boundary::new(18, 0)
761            ],
762        );
763        assert_eq!(
764            wrapper.wrap_line("     aaaaaaa", 72.).collect::<Vec<_>>(),
765            &[
766                Boundary::new(7, 5),
767                Boundary::new(9, 5),
768                Boundary::new(11, 5),
769            ]
770        );
771        assert_eq!(
772            wrapper
773                .wrap_line("                            ", 72.)
774                .collect::<Vec<_>>(),
775            &[
776                Boundary::new(7, 0),
777                Boundary::new(14, 0),
778                Boundary::new(21, 0)
779            ]
780        );
781        assert_eq!(
782            wrapper
783                .wrap_line("          aaaaaaaaaaaaaa", 72.)
784                .collect::<Vec<_>>(),
785            &[
786                Boundary::new(7, 0),
787                Boundary::new(14, 3),
788                Boundary::new(18, 3),
789                Boundary::new(22, 3),
790            ]
791        );
792    }
793
794    #[crate::test(self, retries = 5)]
795    fn test_wrap_shaped_line(cx: &mut crate::AppContext) {
796        // This is failing intermittently on CI and we don't have time to figure it out
797        let font_cache = cx.font_cache().clone();
798        let font_system = cx.platform().fonts();
799        let text_layout_cache = TextLayoutCache::new(font_system.clone());
800
801        let family = font_cache
802            .load_family(&["Helvetica"], &Default::default())
803            .unwrap();
804        let font_id = font_cache.select_font(family, &Default::default()).unwrap();
805        let normal = RunStyle {
806            font_id,
807            color: Default::default(),
808            underline: Default::default(),
809        };
810        let bold = RunStyle {
811            font_id: font_cache
812                .select_font(
813                    family,
814                    &Properties {
815                        weight: Weight::BOLD,
816                        ..Default::default()
817                    },
818                )
819                .unwrap(),
820            color: Default::default(),
821            underline: Default::default(),
822        };
823
824        let text = "aa bbb cccc ddddd eeee";
825        let line = text_layout_cache.layout_str(
826            text,
827            16.0,
828            &[(4, normal), (5, bold), (6, normal), (1, bold), (7, normal)],
829        );
830
831        let mut wrapper = LineWrapper::new(font_id, 16., font_system);
832        assert_eq!(
833            wrapper
834                .wrap_shaped_line(text, &line, 72.0)
835                .collect::<Vec<_>>(),
836            &[
837                ShapedBoundary {
838                    run_ix: 1,
839                    glyph_ix: 3
840                },
841                ShapedBoundary {
842                    run_ix: 2,
843                    glyph_ix: 3
844                },
845                ShapedBoundary {
846                    run_ix: 4,
847                    glyph_ix: 2
848                }
849            ],
850        );
851    }
852}