1use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun, px};
2use collections::HashMap;
3use std::{borrow::Cow, iter, sync::Arc};
4
5/// The GPUI line wrapper, used to wrap lines of text to a given width.
6pub struct LineWrapper {
7 platform_text_system: Arc<dyn PlatformTextSystem>,
8 pub(crate) font_id: FontId,
9 pub(crate) font_size: Pixels,
10 cached_ascii_char_widths: [Option<Pixels>; 128],
11 cached_other_char_widths: HashMap<char, Pixels>,
12}
13
14impl LineWrapper {
15 /// The maximum indent that can be applied to a line.
16 pub const MAX_INDENT: u32 = 256;
17
18 pub(crate) fn new(
19 font_id: FontId,
20 font_size: Pixels,
21 text_system: Arc<dyn PlatformTextSystem>,
22 ) -> Self {
23 Self {
24 platform_text_system: text_system,
25 font_id,
26 font_size,
27 cached_ascii_char_widths: [None; 128],
28 cached_other_char_widths: HashMap::default(),
29 }
30 }
31
32 /// Wrap a line of text to the given width with this wrapper's font and font size.
33 pub fn wrap_line<'a>(
34 &'a mut self,
35 fragments: &'a [LineFragment],
36 wrap_width: Pixels,
37 ) -> impl Iterator<Item = Boundary> + 'a {
38 let mut width = px(0.);
39 let mut first_non_whitespace_ix = None;
40 let mut indent = None;
41 let mut last_candidate_ix = 0;
42 let mut last_candidate_width = px(0.);
43 let mut last_wrap_ix = 0;
44 let mut prev_c = '\0';
45 let mut index = 0;
46 let mut candidates = fragments
47 .iter()
48 .flat_map(move |fragment| fragment.wrap_boundary_candidates())
49 .peekable();
50 iter::from_fn(move || {
51 for candidate in candidates.by_ref() {
52 let ix = index;
53 index += candidate.len_utf8();
54 let mut new_prev_c = prev_c;
55 let item_width = match candidate {
56 WrapBoundaryCandidate::Char { character: c } => {
57 if c == '\n' {
58 continue;
59 }
60
61 if Self::is_word_char(c) {
62 if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
63 last_candidate_ix = ix;
64 last_candidate_width = width;
65 }
66 } else {
67 // CJK may not be space separated, e.g.: `Hello world你好世界`
68 if c != ' ' && first_non_whitespace_ix.is_some() {
69 last_candidate_ix = ix;
70 last_candidate_width = width;
71 }
72 }
73
74 if c != ' ' && first_non_whitespace_ix.is_none() {
75 first_non_whitespace_ix = Some(ix);
76 }
77
78 new_prev_c = c;
79
80 self.width_for_char(c)
81 }
82 WrapBoundaryCandidate::Element {
83 width: element_width,
84 ..
85 } => {
86 if prev_c == ' ' && first_non_whitespace_ix.is_some() {
87 last_candidate_ix = ix;
88 last_candidate_width = width;
89 }
90
91 if first_non_whitespace_ix.is_none() {
92 first_non_whitespace_ix = Some(ix);
93 }
94
95 element_width
96 }
97 };
98
99 width += item_width;
100 if width > wrap_width && ix > last_wrap_ix {
101 if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
102 {
103 indent = Some(
104 Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
105 );
106 }
107
108 if last_candidate_ix > 0 {
109 last_wrap_ix = last_candidate_ix;
110 width -= last_candidate_width;
111 last_candidate_ix = 0;
112 } else {
113 last_wrap_ix = ix;
114 width = item_width;
115 }
116
117 if let Some(indent) = indent {
118 width += self.width_for_char(' ') * indent as f32;
119 }
120
121 return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
122 }
123
124 prev_c = new_prev_c;
125 }
126
127 None
128 })
129 }
130
131 /// Determines if a line should be truncated based on its width.
132 pub fn should_truncate_line(
133 &mut self,
134 line: &str,
135 truncate_width: Pixels,
136 truncation_suffix: &str,
137 ) -> Option<usize> {
138 let mut width = px(0.);
139 let suffix_width = truncation_suffix
140 .chars()
141 .map(|c| self.width_for_char(c))
142 .fold(px(0.0), |a, x| a + x);
143 let mut truncate_ix = 0;
144
145 for (ix, c) in line.char_indices() {
146 if width + suffix_width < truncate_width {
147 truncate_ix = ix;
148 }
149
150 let char_width = self.width_for_char(c);
151 width += char_width;
152
153 if width.floor() > truncate_width {
154 return Some(truncate_ix);
155 }
156 }
157
158 None
159 }
160
161 /// Truncate a line of text to the given width with this wrapper's font and font size.
162 pub fn truncate_line<'a>(
163 &mut self,
164 line: SharedString,
165 truncate_width: Pixels,
166 truncation_suffix: &str,
167 runs: &'a [TextRun],
168 ) -> (SharedString, Cow<'a, [TextRun]>) {
169 if let Some(truncate_ix) =
170 self.should_truncate_line(&line, truncate_width, truncation_suffix)
171 {
172 let result =
173 SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
174 let mut runs = runs.to_vec();
175 update_runs_after_truncation(&result, truncation_suffix, &mut runs);
176 (result, Cow::Owned(runs))
177 } else {
178 (line, Cow::Borrowed(runs))
179 }
180 }
181
182 /// Any character in this list should be treated as a word character,
183 /// meaning it can be part of a word that should not be wrapped.
184 pub(crate) fn is_word_char(c: char) -> bool {
185 // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
186 c.is_ascii_alphanumeric() ||
187 // Latin script in Unicode for French, German, Spanish, etc.
188 // Latin-1 Supplement
189 // https://en.wikipedia.org/wiki/Latin-1_Supplement
190 matches!(c, '\u{00C0}'..='\u{00FF}') ||
191 // Latin Extended-A
192 // https://en.wikipedia.org/wiki/Latin_Extended-A
193 matches!(c, '\u{0100}'..='\u{017F}') ||
194 // Latin Extended-B
195 // https://en.wikipedia.org/wiki/Latin_Extended-B
196 matches!(c, '\u{0180}'..='\u{024F}') ||
197 // Cyrillic for Russian, Ukrainian, etc.
198 // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
199 matches!(c, '\u{0400}'..='\u{04FF}') ||
200
201 // Vietnamese (https://vietunicode.sourceforge.net/charset/)
202 matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
203 matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
204
205 // Some other known special characters that should be treated as word characters,
206 // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
207 // `2^3`, `a~b`, `a=1`, `Self::new`, etc.
208 matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':') ||
209 // `⋯` character is special used in Zed, to keep this at the end of the line.
210 matches!(c, '⋯')
211 }
212
213 #[inline(always)]
214 fn width_for_char(&mut self, c: char) -> Pixels {
215 if (c as u32) < 128 {
216 if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
217 cached_width
218 } else {
219 let width = self.compute_width_for_char(c);
220 self.cached_ascii_char_widths[c as usize] = Some(width);
221 width
222 }
223 } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
224 *cached_width
225 } else {
226 let width = self.compute_width_for_char(c);
227 self.cached_other_char_widths.insert(c, width);
228 width
229 }
230 }
231
232 fn compute_width_for_char(&self, c: char) -> Pixels {
233 let mut buffer = [0; 4];
234 let buffer = c.encode_utf8(&mut buffer);
235 self.platform_text_system
236 .layout_line(
237 buffer,
238 self.font_size,
239 &[FontRun {
240 len: buffer.len(),
241 font_id: self.font_id,
242 }],
243 )
244 .width
245 }
246}
247
248fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
249 let mut truncate_at = result.len() - ellipsis.len();
250 for (run_index, run) in runs.iter_mut().enumerate() {
251 if run.len <= truncate_at {
252 truncate_at -= run.len;
253 } else {
254 run.len = truncate_at + ellipsis.len();
255 runs.truncate(run_index + 1);
256 break;
257 }
258 }
259}
260
261/// A fragment of a line that can be wrapped.
262pub enum LineFragment<'a> {
263 /// A text fragment consisting of characters.
264 Text {
265 /// The text content of the fragment.
266 text: &'a str,
267 },
268 /// A non-text element with a fixed width.
269 Element {
270 /// The width of the element in pixels.
271 width: Pixels,
272 /// The UTF-8 encoded length of the element.
273 len_utf8: usize,
274 },
275}
276
277impl<'a> LineFragment<'a> {
278 /// Creates a new text fragment from the given text.
279 pub fn text(text: &'a str) -> Self {
280 LineFragment::Text { text }
281 }
282
283 /// Creates a new non-text element with the given width and UTF-8 encoded length.
284 pub fn element(width: Pixels, len_utf8: usize) -> Self {
285 LineFragment::Element { width, len_utf8 }
286 }
287
288 fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
289 let text = match self {
290 LineFragment::Text { text } => text,
291 LineFragment::Element { .. } => "\0",
292 };
293 text.chars().map(move |character| {
294 if let LineFragment::Element { width, len_utf8 } = self {
295 WrapBoundaryCandidate::Element {
296 width: *width,
297 len_utf8: *len_utf8,
298 }
299 } else {
300 WrapBoundaryCandidate::Char { character }
301 }
302 })
303 }
304}
305
306enum WrapBoundaryCandidate {
307 Char { character: char },
308 Element { width: Pixels, len_utf8: usize },
309}
310
311impl WrapBoundaryCandidate {
312 pub fn len_utf8(&self) -> usize {
313 match self {
314 WrapBoundaryCandidate::Char { character } => character.len_utf8(),
315 WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
316 }
317 }
318}
319
320/// A boundary between two lines of text.
321#[derive(Copy, Clone, Debug, PartialEq, Eq)]
322pub struct Boundary {
323 /// The index of the last character in a line
324 pub ix: usize,
325 /// The indent of the next line.
326 pub next_indent: u32,
327}
328
329impl Boundary {
330 fn new(ix: usize, next_indent: u32) -> Self {
331 Self { ix, next_indent }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
339 #[cfg(target_os = "macos")]
340 use crate::{TextRun, WindowTextSystem, WrapBoundary};
341 use rand::prelude::*;
342
343 fn build_wrapper() -> LineWrapper {
344 let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
345 let cx = TestAppContext::build(dispatcher, None);
346 let id = cx.text_system().resolve_font(&font(".ZedMono"));
347 LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())
348 }
349
350 fn generate_test_runs(input_run_len: &[usize]) -> Vec<TextRun> {
351 input_run_len
352 .iter()
353 .map(|run_len| TextRun {
354 len: *run_len,
355 font: Font {
356 family: "Dummy".into(),
357 features: FontFeatures::default(),
358 fallbacks: None,
359 weight: FontWeight::default(),
360 style: FontStyle::Normal,
361 },
362 ..Default::default()
363 })
364 .collect()
365 }
366
367 #[test]
368 fn test_wrap_line() {
369 let mut wrapper = build_wrapper();
370
371 assert_eq!(
372 wrapper
373 .wrap_line(&[LineFragment::text("aa bbb cccc ddddd eeee")], px(72.))
374 .collect::<Vec<_>>(),
375 &[
376 Boundary::new(7, 0),
377 Boundary::new(12, 0),
378 Boundary::new(18, 0)
379 ],
380 );
381 assert_eq!(
382 wrapper
383 .wrap_line(&[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], px(72.0))
384 .collect::<Vec<_>>(),
385 &[
386 Boundary::new(4, 0),
387 Boundary::new(11, 0),
388 Boundary::new(18, 0)
389 ],
390 );
391 assert_eq!(
392 wrapper
393 .wrap_line(&[LineFragment::text(" aaaaaaa")], px(72.))
394 .collect::<Vec<_>>(),
395 &[
396 Boundary::new(7, 5),
397 Boundary::new(9, 5),
398 Boundary::new(11, 5),
399 ]
400 );
401 assert_eq!(
402 wrapper
403 .wrap_line(
404 &[LineFragment::text(" ")],
405 px(72.)
406 )
407 .collect::<Vec<_>>(),
408 &[
409 Boundary::new(7, 0),
410 Boundary::new(14, 0),
411 Boundary::new(21, 0)
412 ]
413 );
414 assert_eq!(
415 wrapper
416 .wrap_line(&[LineFragment::text(" aaaaaaaaaaaaaa")], px(72.))
417 .collect::<Vec<_>>(),
418 &[
419 Boundary::new(7, 0),
420 Boundary::new(14, 3),
421 Boundary::new(18, 3),
422 Boundary::new(22, 3),
423 ]
424 );
425
426 // Test wrapping multiple text fragments
427 assert_eq!(
428 wrapper
429 .wrap_line(
430 &[
431 LineFragment::text("aa bbb "),
432 LineFragment::text("cccc ddddd eeee")
433 ],
434 px(72.)
435 )
436 .collect::<Vec<_>>(),
437 &[
438 Boundary::new(7, 0),
439 Boundary::new(12, 0),
440 Boundary::new(18, 0)
441 ],
442 );
443
444 // Test wrapping with a mix of text and element fragments
445 assert_eq!(
446 wrapper
447 .wrap_line(
448 &[
449 LineFragment::text("aa "),
450 LineFragment::element(px(20.), 1),
451 LineFragment::text(" bbb "),
452 LineFragment::element(px(30.), 1),
453 LineFragment::text(" cccc")
454 ],
455 px(72.)
456 )
457 .collect::<Vec<_>>(),
458 &[
459 Boundary::new(5, 0),
460 Boundary::new(9, 0),
461 Boundary::new(11, 0)
462 ],
463 );
464
465 // Test with element at the beginning and text afterward
466 assert_eq!(
467 wrapper
468 .wrap_line(
469 &[
470 LineFragment::element(px(50.), 1),
471 LineFragment::text(" aaaa bbbb cccc dddd")
472 ],
473 px(72.)
474 )
475 .collect::<Vec<_>>(),
476 &[
477 Boundary::new(2, 0),
478 Boundary::new(7, 0),
479 Boundary::new(12, 0),
480 Boundary::new(17, 0)
481 ],
482 );
483
484 // Test with a large element that forces wrapping by itself
485 assert_eq!(
486 wrapper
487 .wrap_line(
488 &[
489 LineFragment::text("short text "),
490 LineFragment::element(px(100.), 1),
491 LineFragment::text(" more text")
492 ],
493 px(72.)
494 )
495 .collect::<Vec<_>>(),
496 &[
497 Boundary::new(6, 0),
498 Boundary::new(11, 0),
499 Boundary::new(12, 0),
500 Boundary::new(18, 0)
501 ],
502 );
503 }
504
505 #[test]
506 fn test_truncate_line() {
507 let mut wrapper = build_wrapper();
508
509 fn perform_test(
510 wrapper: &mut LineWrapper,
511 text: &'static str,
512 expected: &'static str,
513 ellipsis: &str,
514 ) {
515 let dummy_run_lens = vec![text.len()];
516 let dummy_runs = generate_test_runs(&dummy_run_lens);
517 let (result, dummy_runs) =
518 wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
519 assert_eq!(result, expected);
520 assert_eq!(dummy_runs.first().unwrap().len, result.len());
521 }
522
523 perform_test(
524 &mut wrapper,
525 "aa bbb cccc ddddd eeee ffff gggg",
526 "aa bbb cccc ddddd eeee",
527 "",
528 );
529 perform_test(
530 &mut wrapper,
531 "aa bbb cccc ddddd eeee ffff gggg",
532 "aa bbb cccc ddddd eee…",
533 "…",
534 );
535 perform_test(
536 &mut wrapper,
537 "aa bbb cccc ddddd eeee ffff gggg",
538 "aa bbb cccc dddd......",
539 "......",
540 );
541 }
542
543 #[test]
544 fn test_truncate_multiple_runs() {
545 let mut wrapper = build_wrapper();
546
547 fn perform_test(
548 wrapper: &mut LineWrapper,
549 text: &'static str,
550 expected: &str,
551 run_lens: &[usize],
552 result_run_len: &[usize],
553 line_width: Pixels,
554 ) {
555 let dummy_runs = generate_test_runs(run_lens);
556 let (result, dummy_runs) =
557 wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
558 assert_eq!(result, expected);
559 for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
560 assert_eq!(run.len, *result_len);
561 }
562 }
563 // Case 0: Normal
564 // Text: abcdefghijkl
565 // Runs: Run0 { len: 12, ... }
566 //
567 // Truncate res: abcd… (truncate_at = 4)
568 // Run res: Run0 { string: abcd…, len: 7, ... }
569 perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
570 // Case 1: Drop some runs
571 // Text: abcdefghijkl
572 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
573 //
574 // Truncate res: abcdef… (truncate_at = 6)
575 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
576 // 5, ... }
577 perform_test(
578 &mut wrapper,
579 "abcdefghijkl",
580 "abcdef…",
581 &[4, 4, 4],
582 &[4, 5],
583 px(70.),
584 );
585 // Case 2: Truncate at start of some run
586 // Text: abcdefghijkl
587 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
588 //
589 // Truncate res: abcdefgh… (truncate_at = 8)
590 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
591 // 4, ... }, Run2 { string: …, len: 3, ... }
592 perform_test(
593 &mut wrapper,
594 "abcdefghijkl",
595 "abcdefgh…",
596 &[4, 4, 4],
597 &[4, 4, 3],
598 px(90.),
599 );
600 }
601
602 #[test]
603 fn test_update_run_after_truncation() {
604 fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
605 let mut dummy_runs = generate_test_runs(run_lens);
606 update_runs_after_truncation(result, "…", &mut dummy_runs);
607 for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
608 assert_eq!(run.len, *result_len);
609 }
610 }
611 // Case 0: Normal
612 // Text: abcdefghijkl
613 // Runs: Run0 { len: 12, ... }
614 //
615 // Truncate res: abcd… (truncate_at = 4)
616 // Run res: Run0 { string: abcd…, len: 7, ... }
617 perform_test("abcd…", &[12], &[7]);
618 // Case 1: Drop some runs
619 // Text: abcdefghijkl
620 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
621 //
622 // Truncate res: abcdef… (truncate_at = 6)
623 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
624 // 5, ... }
625 perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
626 // Case 2: Truncate at start of some run
627 // Text: abcdefghijkl
628 // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
629 //
630 // Truncate res: abcdefgh… (truncate_at = 8)
631 // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
632 // 4, ... }, Run2 { string: …, len: 3, ... }
633 perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
634 }
635
636 #[test]
637 fn test_is_word_char() {
638 #[track_caller]
639 fn assert_word(word: &str) {
640 for c in word.chars() {
641 assert!(
642 LineWrapper::is_word_char(c),
643 "assertion failed for '{}' (unicode 0x{:x})",
644 c,
645 c as u32
646 );
647 }
648 }
649
650 #[track_caller]
651 fn assert_not_word(word: &str) {
652 let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
653 assert!(found, "assertion failed for '{}'", word);
654 }
655
656 assert_word("Hello123");
657 assert_word("non-English");
658 assert_word("var_name");
659 assert_word("123456");
660 assert_word("3.1415");
661 assert_word("10^2");
662 assert_word("1~2");
663 assert_word("100%");
664 assert_word("@mention");
665 assert_word("#hashtag");
666 assert_word("$variable");
667 assert_word("a=1");
668 assert_word("Self::is_word_char");
669 assert_word("more⋯");
670
671 // Space
672 assert_not_word("foo bar");
673
674 // URL case
675 assert_word("github.com");
676 assert_not_word("zed-industries/zed");
677 assert_not_word("zed-industries\\zed");
678 assert_not_word("a=1&b=2");
679 assert_not_word("foo?b=2");
680
681 // Latin-1 Supplement
682 assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
683 // Latin Extended-A
684 assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
685 // Latin Extended-B
686 assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
687 // Cyrillic
688 assert_word("АБВГДЕЖЗИЙКЛМНОП");
689 // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
690 assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
691
692 // non-word characters
693 assert_not_word("你好");
694 assert_not_word("안녕하세요");
695 assert_not_word("こんにちは");
696 assert_not_word("😀😁😂");
697 assert_not_word("()[]{}<>");
698 }
699
700 // For compatibility with the test macro
701 #[cfg(target_os = "macos")]
702 use crate as gpui;
703
704 // These seem to vary wildly based on the text system.
705 #[cfg(target_os = "macos")]
706 #[crate::test]
707 fn test_wrap_shaped_line(cx: &mut TestAppContext) {
708 cx.update(|cx| {
709 let text_system = WindowTextSystem::new(cx.text_system().clone());
710
711 let normal = TextRun {
712 len: 0,
713 font: font("Helvetica"),
714 color: Default::default(),
715 underline: Default::default(),
716 ..Default::default()
717 };
718 let bold = TextRun {
719 len: 0,
720 font: font("Helvetica").bold(),
721 ..Default::default()
722 };
723
724 let text = "aa bbb cccc ddddd eeee".into();
725 let lines = text_system
726 .shape_text(
727 text,
728 px(16.),
729 &[
730 normal.with_len(4),
731 bold.with_len(5),
732 normal.with_len(6),
733 bold.with_len(1),
734 normal.with_len(7),
735 ],
736 Some(px(72.)),
737 None,
738 )
739 .unwrap();
740
741 assert_eq!(
742 lines[0].layout.wrap_boundaries(),
743 &[
744 WrapBoundary {
745 run_ix: 0,
746 glyph_ix: 7
747 },
748 WrapBoundary {
749 run_ix: 0,
750 glyph_ix: 12
751 },
752 WrapBoundary {
753 run_ix: 0,
754 glyph_ix: 18
755 }
756 ],
757 );
758 });
759 }
760}