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