text.rs

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