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