1use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TruncateFrom {
8 Start,
10 End,
12}
13
14pub struct LineWrapper {
16 text_system: Arc<TextSystem>,
17 pub(crate) font_id: FontId,
18 pub(crate) font_size: Pixels,
19 cached_ascii_char_widths: [Option<Pixels>; 128],
20 cached_other_char_widths: HashMap<char, Pixels>,
21}
22
23impl LineWrapper {
24 pub const MAX_INDENT: u32 = 256;
26
27 pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc<TextSystem>) -> Self {
28 Self {
29 text_system,
30 font_id,
31 font_size,
32 cached_ascii_char_widths: [None; 128],
33 cached_other_char_widths: HashMap::default(),
34 }
35 }
36
37 pub fn wrap_line<'a>(
39 &'a mut self,
40 fragments: &'a [LineFragment],
41 wrap_width: Pixels,
42 ) -> impl Iterator<Item = Boundary> + 'a {
43 let mut width = px(0.);
44 let mut first_non_whitespace_ix = None;
45 let mut indent = None;
46 let mut last_candidate_ix = 0;
47 let mut last_candidate_width = px(0.);
48 let mut last_wrap_ix = 0;
49 let mut prev_c = '\0';
50 let mut index = 0;
51 let mut candidates = fragments
52 .iter()
53 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
54 .peekable();
55 iter::from_fn(move || {
56 for candidate in candidates.by_ref() {
57 let ix = index;
58 index += candidate.len_utf8();
59 let mut new_prev_c = prev_c;
60 let item_width = match candidate {
61 WrapBoundaryCandidate::Char { character: c } => {
62 if c == '\n' {
63 continue;
64 }
65
66 if Self::is_word_char(c) {
67 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
68 last_candidate_ix = ix;
69 last_candidate_width = width;
70 }
71 } else {
72 if c != ' ' && first_non_whitespace_ix.is_some() {
74 last_candidate_ix = ix;
75 last_candidate_width = width;
76 }
77 }
78
79 if c != ' ' && first_non_whitespace_ix.is_none() {
80 first_non_whitespace_ix = Some(ix);
81 }
82
83 new_prev_c = c;
84
85 self.width_for_char(c)
86 }
87 WrapBoundaryCandidate::Element {
88 width: element_width,
89 ..
90 } => {
91 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
92 last_candidate_ix = ix;
93 last_candidate_width = width;
94 }
95
96 if first_non_whitespace_ix.is_none() {
97 first_non_whitespace_ix = Some(ix);
98 }
99
100 element_width
101 }
102 };
103
104 width += item_width;
105 if width > wrap_width && ix > last_wrap_ix {
106 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
107 {
108 indent = Some(
109 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
110 );
111 }
112
113 if last_candidate_ix > 0 {
114 last_wrap_ix = last_candidate_ix;
115 width -= last_candidate_width;
116 last_candidate_ix = 0;
117 } else {
118 last_wrap_ix = ix;
119 width = item_width;
120 }
121
122 if let Some(indent) = indent {
123 width += self.width_for_char(' ') * indent as f32;
124 }
125
126 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
127 }
128
129 prev_c = new_prev_c;
130 }
131
132 None
133 })
134 }
135
136 pub fn should_truncate_line(
140 &mut self,
141 line: &str,
142 truncate_width: Pixels,
143 truncation_affix: &str,
144 truncate_from: TruncateFrom,
145 ) -> Option<usize> {
146 let mut width = px(0.);
147 let suffix_width = truncation_affix
148 .chars()
149 .map(|c| self.width_for_char(c))
150 .fold(px(0.0), |a, x| a + x);
151 let mut truncate_ix = 0;
152
153 match truncate_from {
154 TruncateFrom::Start => {
155 for (ix, c) in line.char_indices().rev() {
156 if width + suffix_width < truncate_width {
157 truncate_ix = ix;
158 }
159
160 let char_width = self.width_for_char(c);
161 width += char_width;
162
163 if width.floor() > truncate_width {
164 return Some(truncate_ix);
165 }
166 }
167 }
168 TruncateFrom::End => {
169 for (ix, c) in line.char_indices() {
170 if width + suffix_width < truncate_width {
171 truncate_ix = ix;
172 }
173
174 let char_width = self.width_for_char(c);
175 width += char_width;
176
177 if width.floor() > truncate_width {
178 return Some(truncate_ix);
179 }
180 }
181 }
182 }
183
184 None
185 }
186
187 pub fn truncate_line<'a>(
189 &mut self,
190 line: SharedString,
191 truncate_width: Pixels,
192 truncation_affix: &str,
193 runs: &'a [TextRun],
194 truncate_from: TruncateFrom,
195 ) -> (SharedString, Cow<'a, [TextRun]>) {
196 if let Some(truncate_ix) =
197 self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
198 {
199 let result = match truncate_from {
200 TruncateFrom::Start => SharedString::from(format!(
201 "{truncation_affix}{}",
202 &line[line.ceil_char_boundary(truncate_ix + 1)..]
203 )),
204 TruncateFrom::End => SharedString::from(format!(
205 "{}{truncation_affix}",
206 line[..truncate_ix]
207 .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
208 )),
209 };
210 let mut runs = runs.to_vec();
211 update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
212 (result, Cow::Owned(runs))
213 } else {
214 (line, Cow::Borrowed(runs))
215 }
216 }
217
218 pub(crate) fn is_word_char(c: char) -> bool {
221 c.is_ascii_alphanumeric() ||
223 matches!(c, '\u{00C0}'..='\u{00FF}') ||
227 matches!(c, '\u{0100}'..='\u{017F}') ||
230 matches!(c, '\u{0180}'..='\u{024F}') ||
233 matches!(c, '\u{0400}'..='\u{04FF}') ||
236
237 matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') || matches!(c, '\u{0980}'..='\u{09FF}') ||
243
244 matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
249 matches!(c, '⋯')
251 }
252
253 #[inline(always)]
254 fn width_for_char(&mut self, c: char) -> Pixels {
255 if (c as u32) < 128 {
256 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
257 cached_width
258 } else {
259 let width = self
260 .text_system
261 .layout_width(self.font_id, self.font_size, c);
262 self.cached_ascii_char_widths[c as usize] = Some(width);
263 width
264 }
265 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
266 *cached_width
267 } else {
268 let width = self
269 .text_system
270 .layout_width(self.font_id, self.font_size, c);
271 self.cached_other_char_widths.insert(c, width);
272 width
273 }
274 }
275}
276
277fn update_runs_after_truncation(
278 result: &str,
279 ellipsis: &str,
280 runs: &mut Vec<TextRun>,
281 truncate_from: TruncateFrom,
282) {
283 let mut truncate_at = result.len() - ellipsis.len();
284 match truncate_from {
285 TruncateFrom::Start => {
286 for (run_index, run) in runs.iter_mut().enumerate().rev() {
287 if run.len <= truncate_at {
288 truncate_at -= run.len;
289 } else {
290 run.len = truncate_at + ellipsis.len();
291 runs.splice(..run_index, std::iter::empty());
292 break;
293 }
294 }
295 }
296 TruncateFrom::End => {
297 for (run_index, run) in runs.iter_mut().enumerate() {
298 if run.len <= truncate_at {
299 truncate_at -= run.len;
300 } else {
301 run.len = truncate_at + ellipsis.len();
302 runs.truncate(run_index + 1);
303 break;
304 }
305 }
306 }
307 }
308}
309
310pub enum LineFragment<'a> {
312 Text {
314 text: &'a str,
316 },
317 Element {
319 width: Pixels,
321 len_utf8: usize,
323 },
324}
325
326impl<'a> LineFragment<'a> {
327 pub fn text(text: &'a str) -> Self {
329 LineFragment::Text { text }
330 }
331
332 pub fn element(width: Pixels, len_utf8: usize) -> Self {
334 LineFragment::Element { width, len_utf8 }
335 }
336
337 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
338 let text = match self {
339 LineFragment::Text { text } => text,
340 LineFragment::Element { .. } => "\0",
341 };
342 text.chars().map(move |character| {
343 if let LineFragment::Element { width, len_utf8 } = self {
344 WrapBoundaryCandidate::Element {
345 width: *width,
346 len_utf8: *len_utf8,
347 }
348 } else {
349 WrapBoundaryCandidate::Char { character }
350 }
351 })
352 }
353}
354
355enum WrapBoundaryCandidate {
356 Char { character: char },
357 Element { width: Pixels, len_utf8: usize },
358}
359
360impl WrapBoundaryCandidate {
361 pub fn len_utf8(&self) -> usize {
362 match self {
363 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
364 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
365 }
366 }
367}
368
369#[derive(Copy, Clone, Debug, PartialEq, Eq)]
371pub struct Boundary {
372 pub ix: usize,
374 pub next_indent: u32,
376}
377
378impl Boundary {
379 fn new(ix: usize, next_indent: u32) -> Self {
380 Self { ix, next_indent }
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
388 #[cfg(target_os = "macos")]
389 use crate::{TextRun, WindowTextSystem, WrapBoundary};
390
391 fn build_wrapper() -> LineWrapper {
392 let dispatcher = TestDispatcher::new(0);
393 let cx = TestAppContext::build(dispatcher, None);
394 let id = cx.text_system().resolve_font(&font(".ZedMono"));
395 LineWrapper::new(id, px(16.), cx.text_system().clone())
396 }
397
398 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
399 input_run_len
400 .iter()
401 .map(|run_len| TextRun {
402 len: *run_len,
403 font: Font {
404 family: "Dummy".into(),
405 features: FontFeatures::default(),
406 fallbacks: None,
407 weight: FontWeight::default(),
408 style: FontStyle::Normal,
409 },
410 ..Default::default()
411 })
412 .collect()
413 }
414
415 #[test]
416 fn test_wrap_line() {
417 let mut wrapper = build_wrapper();
418
419 assert_eq!(
420 wrapper
421 .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
422 .collect::<Vec<_>>(),
423 &[
424 Boundary::new(7, 0),
425 Boundary::new(12, 0),
426 Boundary::new(18, 0)
427 ],
428 );
429 assert_eq!(
430 wrapper
431 .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
432 .collect::<Vec<_>>(),
433 &[
434 Boundary::new(4, 0),
435 Boundary::new(11, 0),
436 Boundary::new(18, 0)
437 ],
438 );
439 assert_eq!(
440 wrapper
441 .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
442 .collect::<Vec<_>>(),
443 &[
444 Boundary::new(7, 5),
445 Boundary::new(9, 5),
446 Boundary::new(11, 5),
447 ]
448 );
449 assert_eq!(
450 wrapper
451 .wrap_line(
452 &[LineFragment::text(" ")],
453 px(72.)
454 )
455 .collect::<Vec<_>>(),
456 &[
457 Boundary::new(7, 0),
458 Boundary::new(14, 0),
459 Boundary::new(21, 0)
460 ]
461 );
462 assert_eq!(
463 wrapper
464 .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
465 .collect::<Vec<_>>(),
466 &[
467 Boundary::new(7, 0),
468 Boundary::new(14, 3),
469 Boundary::new(18, 3),
470 Boundary::new(22, 3),
471 ]
472 );
473
474 assert_eq!(
476 wrapper
477 .wrap_line(
478 &[
479 LineFragment::text("aa bbb "),
480 LineFragment::text("cccc ddddd eeee")
481 ],
482 px(72.)
483 )
484 .collect::<Vec<_>>(),
485 &[
486 Boundary::new(7, 0),
487 Boundary::new(12, 0),
488 Boundary::new(18, 0)
489 ],
490 );
491
492 assert_eq!(
494 wrapper
495 .wrap_line(
496 &[
497 LineFragment::text("aa "),
498 LineFragment::element(px(20.), 1),
499 LineFragment::text(" bbb "),
500 LineFragment::element(px(30.), 1),
501 LineFragment::text(" cccc")
502 ],
503 px(72.)
504 )
505 .collect::<Vec<_>>(),
506 &[
507 Boundary::new(5, 0),
508 Boundary::new(9, 0),
509 Boundary::new(11, 0)
510 ],
511 );
512
513 assert_eq!(
515 wrapper
516 .wrap_line(
517 &[
518 LineFragment::element(px(50.), 1),
519 LineFragment::text(" aaaa bbbb cccc dddd")
520 ],
521 px(72.)
522 )
523 .collect::<Vec<_>>(),
524 &[
525 Boundary::new(2, 0),
526 Boundary::new(7, 0),
527 Boundary::new(12, 0),
528 Boundary::new(17, 0)
529 ],
530 );
531
532 assert_eq!(
534 wrapper
535 .wrap_line(
536 &[
537 LineFragment::text("short text "),
538 LineFragment::element(px(100.), 1),
539 LineFragment::text(" more text")
540 ],
541 px(72.)
542 )
543 .collect::<Vec<_>>(),
544 &[
545 Boundary::new(6, 0),
546 Boundary::new(11, 0),
547 Boundary::new(12, 0),
548 Boundary::new(18, 0)
549 ],
550 );
551 }
552
553 #[test]
554 fn test_truncate_line_end() {
555 let mut wrapper = build_wrapper();
556
557 fn perform_test(
558 wrapper: &mut LineWrapper,
559 text: &'static str,
560 expected: &'static str,
561 ellipsis: &str,
562 ) {
563 let dummy_run_lens = vec![text.len()];
564 let dummy_runs = generate_test_runs(&dummy_run_lens);
565 let (result, dummy_runs) = wrapper.truncate_line(
566 text.into(),
567 px(220.),
568 ellipsis,
569 &dummy_runs,
570 TruncateFrom::End,
571 );
572 assert_eq!(result, expected);
573 assert_eq!(dummy_runs.first().unwrap().len, result.len());
574 }
575
576 perform_test(
577 &mut wrapper,
578 "aa bbb cccc ddddd eeee ffff gggg",
579 "aa bbb cccc ddddd eeee",
580 "",
581 );
582 perform_test(
583 &mut wrapper,
584 "aa bbb cccc ddddd eeee ffff gggg",
585 "aa bbb cccc ddddd eee…",
586 "…",
587 );
588 perform_test(
589 &mut wrapper,
590 "aa bbb cccc ddddd eeee ffff gggg",
591 "aa bbb cccc dddd......",
592 "......",
593 );
594 perform_test(
595 &mut wrapper,
596 "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg",
597 "aa bbb cccc 🦀🦀🦀🦀…",
598 "…",
599 );
600 }
601
602 #[test]
603 fn test_truncate_line_start() {
604 let mut wrapper = build_wrapper();
605
606 #[track_caller]
607 fn perform_test(
608 wrapper: &mut LineWrapper,
609 text: &'static str,
610 expected: &'static str,
611 ellipsis: &str,
612 ) {
613 let dummy_run_lens = vec![text.len()];
614 let dummy_runs = generate_test_runs(&dummy_run_lens);
615 let (result, dummy_runs) = wrapper.truncate_line(
616 text.into(),
617 px(220.),
618 ellipsis,
619 &dummy_runs,
620 TruncateFrom::Start,
621 );
622 assert_eq!(result, expected);
623 assert_eq!(dummy_runs.first().unwrap().len, result.len());
624 }
625
626 perform_test(
627 &mut wrapper,
628 "aaaa bbbb cccc ddddd eeee fff gg",
629 "cccc ddddd eeee fff gg",
630 "",
631 );
632 perform_test(
633 &mut wrapper,
634 "aaaa bbbb cccc ddddd eeee fff gg",
635 "…ccc ddddd eeee fff gg",
636 "…",
637 );
638 perform_test(
639 &mut wrapper,
640 "aaaa bbbb cccc ddddd eeee fff gg",
641 "......dddd eeee fff gg",
642 "......",
643 );
644 perform_test(
645 &mut wrapper,
646 "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg",
647 "…🦀🦀🦀🦀 eeee fff gg",
648 "…",
649 );
650 }
651
652 #[test]
653 fn test_truncate_multiple_runs_end() {
654 let mut wrapper = build_wrapper();
655
656 fn perform_test(
657 wrapper: &mut LineWrapper,
658 text: &'static str,
659 expected: &str,
660 run_lens: &[usize],
661 result_run_len: &[usize],
662 line_width: Pixels,
663 ) {
664 let dummy_runs = generate_test_runs(run_lens);
665 let (result, dummy_runs) =
666 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
667 assert_eq!(result, expected);
668 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
669 assert_eq!(run.len, *result_len);
670 }
671 }
672 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
679 perform_test(
687 &mut wrapper,
688 "abcdefghijkl",
689 "abcdef…",
690 &[4, 4, 4],
691 &[4, 5],
692 px(70.),
693 );
694 perform_test(
702 &mut wrapper,
703 "abcdefghijkl",
704 "abcdefgh…",
705 &[4, 4, 4],
706 &[4, 4, 3],
707 px(90.),
708 );
709 }
710
711 #[test]
712 fn test_truncate_multiple_runs_start() {
713 let mut wrapper = build_wrapper();
714
715 #[track_caller]
716 fn perform_test(
717 wrapper: &mut LineWrapper,
718 text: &'static str,
719 expected: &str,
720 run_lens: &[usize],
721 result_run_len: &[usize],
722 line_width: Pixels,
723 ) {
724 let dummy_runs = generate_test_runs(run_lens);
725 let (result, dummy_runs) = wrapper.truncate_line(
726 text.into(),
727 line_width,
728 "…",
729 &dummy_runs,
730 TruncateFrom::Start,
731 );
732 assert_eq!(result, expected);
733 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
734 assert_eq!(run.len, *result_len);
735 }
736 }
737 perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
744 perform_test(
752 &mut wrapper,
753 "abcdefghijkl",
754 "…ghijkl",
755 &[4, 4, 4],
756 &[5, 4],
757 px(70.),
758 );
759 perform_test(
767 &mut wrapper,
768 "abcdefghijkl",
769 "…efghijkl",
770 &[4, 4, 4],
771 &[3, 4, 4],
772 px(90.),
773 );
774 }
775
776 #[test]
777 fn test_update_run_after_truncation_end() {
778 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
779 let mut dummy_runs = generate_test_runs(run_lens);
780 update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
781 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
782 assert_eq!(run.len, *result_len);
783 }
784 }
785 perform_test("abcd…", &[12], &[7]);
792 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
800 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
808 }
809
810 #[test]
811 fn test_is_word_char() {
812 #[track_caller]
813 fn assert_word(word: &str) {
814 for c in word.chars() {
815 assert!(
816 LineWrapper::is_word_char(c),
817 "assertion failed for '{}' (unicode 0x{:x})",
818 c,
819 c as u32
820 );
821 }
822 }
823
824 #[track_caller]
825 fn assert_not_word(word: &str) {
826 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
827 assert!(found, "assertion failed for '{}'", word);
828 }
829
830 assert_word("Hello123");
831 assert_word("non-English");
832 assert_word("var_name");
833 assert_word("123456");
834 assert_word("3.1415");
835 assert_word("10^2");
836 assert_word("1~2");
837 assert_word("100%");
838 assert_word("@mention");
839 assert_word("#hashtag");
840 assert_word("$variable");
841 assert_word("a=1");
842 assert_word("Self::is_word_char");
843 assert_word("on;");
844 assert_word("more⋯");
845 assert_word("won’t");
846 assert_word("‘twas");
847
848 assert_not_word("foo bar");
850
851 assert_word("github.com");
853 assert_not_word("zed-industries/zed");
854 assert_not_word("zed-industries\\zed");
855 assert_not_word("a=1&b=2");
856 assert_not_word("foo?b=2");
857
858 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
860 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
862 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
864 assert_word("АБВГДЕЖЗИЙКЛМНОП");
866 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
868 assert_word("গিয়েছিলেন");
870 assert_word("ছেলে");
871 assert_word("হচ্ছিল");
872
873 assert_not_word("你好");
875 assert_not_word("안녕하세요");
876 assert_not_word("こんにちは");
877 assert_not_word("😀😁😂");
878 assert_not_word("()[]{}<>");
879 }
880
881 #[cfg(target_os = "macos")]
883 use crate as gpui;
884
885 #[cfg(target_os = "macos")]
887 #[crate::test]
888 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
889 cx.update(|cx| {
890 let text_system = WindowTextSystem::new(cx.text_system().clone());
891
892 let normal = TextRun {
893 len: 0,
894 font: font("Helvetica"),
895 color: Default::default(),
896 underline: Default::default(),
897 ..Default::default()
898 };
899 let bold = TextRun {
900 len: 0,
901 font: font("Helvetica").bold(),
902 ..Default::default()
903 };
904
905 let text = "aa bbb cccc ddddd eeee".into();
906 let lines = text_system
907 .shape_text(
908 text,
909 px(16.),
910 &[
911 normal.with_len(4),
912 bold.with_len(5),
913 normal.with_len(6),
914 bold.with_len(1),
915 normal.with_len(7),
916 ],
917 Some(px(72.)),
918 None,
919 )
920 .unwrap();
921
922 assert_eq!(
923 lines[0].layout.wrap_boundaries(),
924 &[
925 WrapBoundary {
926 run_ix: 0,
927 glyph_ix: 7
928 },
929 WrapBoundary {
930 run_ix: 0,
931 glyph_ix: 12
932 },
933 WrapBoundary {
934 run_ix: 0,
935 glyph_ix: 18
936 }
937 ],
938 );
939 });
940 }
941}