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