1#![forbid(unsafe_code)]
2
3use crate::shaping::ShapedRun;
49use unicode_segmentation::UnicodeSegmentation;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct ClusterEntry {
58 pub byte_start: u32,
60 pub byte_end: u32,
62 pub grapheme_index: u32,
64 pub cell_start: u32,
66 pub cell_width: u8,
68}
69
70impl ClusterEntry {
71 #[inline]
73 pub fn byte_range(&self) -> std::ops::Range<usize> {
74 self.byte_start as usize..self.byte_end as usize
75 }
76
77 #[inline]
79 pub fn cell_range(&self) -> std::ops::Range<usize> {
80 self.cell_start as usize..(self.cell_start as usize + self.cell_width as usize)
81 }
82
83 #[inline]
85 pub fn cell_end(&self) -> u32 {
86 self.cell_start + self.cell_width as u32
87 }
88}
89
90#[derive(Debug, Clone)]
107pub struct ClusterMap {
108 entries: Vec<ClusterEntry>,
110 total_cells: u32,
112 total_bytes: u32,
114}
115
116impl ClusterMap {
117 pub fn from_text(text: &str) -> Self {
122 if text.is_empty() {
123 return Self {
124 entries: Vec::new(),
125 total_cells: 0,
126 total_bytes: 0,
127 };
128 }
129
130 let mut entries = Vec::with_capacity(text.len());
131 let mut cell_offset = 0u32;
132
133 for (grapheme_idx, (byte_offset, grapheme)) in text.grapheme_indices(true).enumerate() {
134 let width = crate::grapheme_width(grapheme) as u8;
135 let byte_end = byte_offset + grapheme.len();
136
137 entries.push(ClusterEntry {
138 byte_start: byte_offset as u32,
139 byte_end: byte_end as u32,
140 grapheme_index: grapheme_idx as u32,
141 cell_start: cell_offset,
142 cell_width: width,
143 });
144
145 cell_offset += width as u32;
146 }
147
148 Self {
149 entries,
150 total_cells: cell_offset,
151 total_bytes: text.len() as u32,
152 }
153 }
154
155 pub fn from_shaped_run(text: &str, run: &ShapedRun) -> Self {
164 if text.is_empty() || run.is_empty() {
165 return Self {
166 entries: Vec::new(),
167 total_cells: 0,
168 total_bytes: 0,
169 };
170 }
171
172 let mut entries = Vec::new();
176 let mut cell_offset = 0u32;
177 let mut grapheme_idx = 0u32;
178
179 let mut i = 0;
180 while i < run.glyphs.len() {
181 let cluster_byte = run.glyphs[i].cluster as usize;
182 let mut cluster_advance = 0i32;
183
184 let mut j = i;
186 while j < run.glyphs.len() && run.glyphs[j].cluster as usize == cluster_byte {
187 cluster_advance += run.glyphs[j].x_advance;
188 j += 1;
189 }
190
191 let next_byte = if j < run.glyphs.len() {
193 run.glyphs[j].cluster as usize
194 } else {
195 text.len()
196 };
197
198 let width = cluster_advance.unsigned_abs().min(255) as u8;
200
201 entries.push(ClusterEntry {
202 byte_start: cluster_byte as u32,
203 byte_end: next_byte as u32,
204 grapheme_index: grapheme_idx,
205 cell_start: cell_offset,
206 cell_width: width,
207 });
208
209 cell_offset += width as u32;
210 grapheme_idx += 1;
211 i = j;
212 }
213
214 Self {
215 entries,
216 total_cells: cell_offset,
217 total_bytes: text.len() as u32,
218 }
219 }
220
221 pub fn byte_to_cell(&self, byte_offset: usize) -> usize {
230 if self.entries.is_empty() || byte_offset >= self.total_bytes as usize {
231 return self.total_cells as usize;
232 }
233
234 match self
235 .entries
236 .binary_search_by_key(&(byte_offset as u32), |e| e.byte_start)
237 {
238 Ok(idx) => self.entries[idx].cell_start as usize,
239 Err(idx) => {
240 if idx > 0 {
242 self.entries[idx - 1].cell_start as usize
243 } else {
244 0
245 }
246 }
247 }
248 }
249
250 pub fn byte_to_entry(&self, byte_offset: usize) -> Option<&ClusterEntry> {
254 if self.entries.is_empty() {
255 return None;
256 }
257
258 match self
259 .entries
260 .binary_search_by_key(&(byte_offset as u32), |e| e.byte_start)
261 {
262 Ok(idx) => Some(&self.entries[idx]),
263 Err(idx) => {
264 if idx > 0 && (byte_offset as u32) < self.entries[idx - 1].byte_end {
265 Some(&self.entries[idx - 1])
266 } else {
267 None
268 }
269 }
270 }
271 }
272
273 pub fn byte_range_to_cell_range(&self, byte_start: usize, byte_end: usize) -> (usize, usize) {
278 if self.entries.is_empty() || byte_start >= byte_end {
279 return (0, 0);
280 }
281
282 let start_cell = self.byte_to_cell(byte_start);
283
284 let end_cell = if byte_end >= self.total_bytes as usize {
286 self.total_cells as usize
287 } else {
288 match self
289 .entries
290 .binary_search_by_key(&(byte_end as u32), |e| e.byte_start)
291 {
292 Ok(idx) => self.entries[idx].cell_start as usize,
293 Err(idx) => {
294 if idx > 0 {
295 self.entries[idx - 1].cell_end() as usize
296 } else {
297 0
298 }
299 }
300 }
301 };
302
303 (start_cell, end_cell)
304 }
305
306 pub fn cell_to_byte(&self, cell_col: usize) -> usize {
316 if self.entries.is_empty() || cell_col >= self.total_cells as usize {
317 return self.total_bytes as usize;
318 }
319
320 let idx = self
327 .entries
328 .partition_point(|e| (e.cell_start as usize) < cell_col);
329 if idx < self.entries.len() && self.entries[idx].cell_start as usize == cell_col {
330 self.entries[idx].byte_start as usize
331 } else {
332 self.entries[idx - 1].byte_start as usize
335 }
336 }
337
338 pub fn cell_to_entry(&self, cell_col: usize) -> Option<&ClusterEntry> {
342 if self.entries.is_empty() || cell_col >= self.total_cells as usize {
343 return None;
344 }
345
346 let idx = self
350 .entries
351 .partition_point(|e| (e.cell_start as usize) < cell_col);
352 if idx < self.entries.len() && self.entries[idx].cell_start as usize == cell_col {
353 Some(&self.entries[idx])
354 } else {
355 let entry = &self.entries[idx - 1];
359 if (cell_col as u32) < entry.cell_end() {
360 Some(entry)
361 } else {
362 None
363 }
364 }
365 }
366
367 pub fn cell_range_to_byte_range(&self, cell_start: usize, cell_end: usize) -> (usize, usize) {
373 if self.entries.is_empty() || cell_start >= cell_end {
374 return (0, 0);
375 }
376
377 let start_byte = self.cell_to_byte(cell_start);
378
379 let end_byte = if cell_end >= self.total_cells as usize {
380 self.total_bytes as usize
381 } else {
382 match self.cell_to_entry(cell_end.saturating_sub(1)) {
386 Some(entry) => entry.byte_end as usize,
387 None => self.total_bytes as usize,
388 }
389 };
390
391 (start_byte, end_byte.max(start_byte))
392 }
393
394 pub fn grapheme_to_cell(&self, grapheme_index: usize) -> usize {
400 self.entries
401 .get(grapheme_index)
402 .map_or(self.total_cells as usize, |e| e.cell_start as usize)
403 }
404
405 pub fn cell_to_grapheme(&self, cell_col: usize) -> usize {
407 self.cell_to_entry(cell_col)
408 .map_or(self.entries.len(), |e| e.grapheme_index as usize)
409 }
410
411 pub fn grapheme_to_byte(&self, grapheme_index: usize) -> usize {
413 self.entries
414 .get(grapheme_index)
415 .map_or(self.total_bytes as usize, |e| e.byte_start as usize)
416 }
417
418 pub fn byte_to_grapheme(&self, byte_offset: usize) -> usize {
420 self.byte_to_entry(byte_offset)
421 .map_or(self.entries.len(), |e| e.grapheme_index as usize)
422 }
423
424 #[inline]
430 pub fn total_cells(&self) -> usize {
431 self.total_cells as usize
432 }
433
434 #[inline]
436 pub fn total_bytes(&self) -> usize {
437 self.total_bytes as usize
438 }
439
440 #[inline]
442 pub fn cluster_count(&self) -> usize {
443 self.entries.len()
444 }
445
446 #[inline]
448 pub fn is_empty(&self) -> bool {
449 self.entries.is_empty()
450 }
451
452 #[inline]
454 pub fn entries(&self) -> &[ClusterEntry] {
455 &self.entries
456 }
457
458 #[inline]
460 pub fn get(&self, grapheme_index: usize) -> Option<&ClusterEntry> {
461 self.entries.get(grapheme_index)
462 }
463
464 pub fn extract_text_for_cells<'a>(
469 &self,
470 source: &'a str,
471 cell_start: usize,
472 cell_end: usize,
473 ) -> &'a str {
474 let (byte_start, byte_end) = self.cell_range_to_byte_range(cell_start, cell_end);
475 if byte_start >= source.len() {
476 return "";
477 }
478 let end = byte_end.min(source.len());
479 &source[byte_start..end]
480 }
481}
482
483#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
496 fn empty_text() {
497 let map = ClusterMap::from_text("");
498 assert!(map.is_empty());
499 assert_eq!(map.total_cells(), 0);
500 assert_eq!(map.total_bytes(), 0);
501 assert_eq!(map.cluster_count(), 0);
502 }
503
504 #[test]
505 fn ascii_text() {
506 let map = ClusterMap::from_text("Hello");
507 assert_eq!(map.cluster_count(), 5);
508 assert_eq!(map.total_cells(), 5);
509 assert_eq!(map.total_bytes(), 5);
510
511 for i in 0..5 {
513 let e = map.get(i).unwrap();
514 assert_eq!(e.byte_start, i as u32);
515 assert_eq!(e.byte_end, (i + 1) as u32);
516 assert_eq!(e.cell_start, i as u32);
517 assert_eq!(e.cell_width, 1);
518 }
519 }
520
521 #[test]
522 fn wide_chars() {
523 let text = "\u{4E16}\u{754C}";
525 let map = ClusterMap::from_text(text);
526
527 assert_eq!(map.cluster_count(), 2);
528 assert_eq!(map.total_bytes(), 6);
529 assert_eq!(map.total_cells(), 4);
530
531 let e0 = map.get(0).unwrap();
532 assert_eq!(e0.byte_start, 0);
533 assert_eq!(e0.byte_end, 3);
534 assert_eq!(e0.cell_start, 0);
535 assert_eq!(e0.cell_width, 2);
536
537 let e1 = map.get(1).unwrap();
538 assert_eq!(e1.byte_start, 3);
539 assert_eq!(e1.byte_end, 6);
540 assert_eq!(e1.cell_start, 2);
541 assert_eq!(e1.cell_width, 2);
542 }
543
544 #[test]
545 fn mixed_ascii_and_wide() {
546 let text = "Hi\u{4E16}\u{754C}!";
548 let map = ClusterMap::from_text(text);
549
550 assert_eq!(map.cluster_count(), 5);
551 assert_eq!(map.total_bytes(), 9); assert_eq!(map.total_cells(), 7); assert_eq!(map.get(0).unwrap().cell_start, 0); assert_eq!(map.get(1).unwrap().cell_start, 1); assert_eq!(map.get(2).unwrap().cell_start, 2); assert_eq!(map.get(3).unwrap().cell_start, 4); assert_eq!(map.get(4).unwrap().cell_start, 6); }
561
562 #[test]
563 fn zero_width_clusters_round_trip_to_first_owner() {
564 let text = "a\u{200B}\u{200B}b";
572 let map = ClusterMap::from_text(text);
573
574 assert!(
577 map.entries().iter().any(|e| e.cell_width == 0),
578 "expected at least one zero-width cluster"
579 );
580 let cell_starts: Vec<u32> = map.entries().iter().map(|e| e.cell_start).collect();
581 assert!(
582 cell_starts.windows(2).any(|w| w[0] == w[1]),
583 "expected duplicate cell_start columns"
584 );
585
586 for e in map.entries() {
588 let cell = map.byte_to_cell(e.byte_start as usize);
589 let back = map.cell_to_byte(cell);
590 assert!(
591 back <= e.byte_start as usize,
592 "round-trip overshoot: byte {} -> cell {} -> byte {}",
593 e.byte_start,
594 cell,
595 back
596 );
597 }
598
599 let first_at_col1 = map
602 .entries()
603 .iter()
604 .find(|e| e.cell_start == 1)
605 .expect("a cluster at cell column 1");
606 assert_eq!(map.cell_to_byte(1), first_at_col1.byte_start as usize);
607 assert_eq!(
608 map.cell_to_entry(1).map(|e| e.byte_start),
609 Some(first_at_col1.byte_start)
610 );
611 }
612
613 #[test]
614 fn fuzz_crash_control_chars_round_trip() {
615 let text = "2\u{03}\n";
622 let map = ClusterMap::from_text(text);
623
624 for e in map.entries() {
625 let cell = map.byte_to_cell(e.byte_start as usize);
626 let back = map.cell_to_byte(cell);
627 assert!(
628 back <= e.byte_start as usize,
629 "round-trip overshoot: byte {} -> cell {} -> byte {}",
630 e.byte_start,
631 cell,
632 back
633 );
634 }
635
636 assert_eq!(map.cell_to_byte(1), 1);
638 }
639
640 #[test]
641 fn combining_marks() {
642 let text = "e\u{0301}";
644 let map = ClusterMap::from_text(text);
645
646 assert_eq!(map.cluster_count(), 1);
647 assert_eq!(map.total_bytes(), 3); assert_eq!(map.total_cells(), 1);
649
650 let e = map.get(0).unwrap();
651 assert_eq!(e.byte_start, 0);
652 assert_eq!(e.byte_end, 3);
653 assert_eq!(e.cell_width, 1);
654 }
655
656 #[test]
661 fn byte_to_cell_ascii() {
662 let map = ClusterMap::from_text("Hello");
663 for i in 0..5 {
664 assert_eq!(map.byte_to_cell(i), i);
665 }
666 assert_eq!(map.byte_to_cell(5), 5); }
668
669 #[test]
670 fn byte_to_cell_wide() {
671 let text = "Hi\u{4E16}\u{754C}!";
672 let map = ClusterMap::from_text(text);
673
674 assert_eq!(map.byte_to_cell(0), 0); assert_eq!(map.byte_to_cell(1), 1); assert_eq!(map.byte_to_cell(2), 2); assert_eq!(map.byte_to_cell(5), 4); assert_eq!(map.byte_to_cell(8), 6); }
680
681 #[test]
682 fn byte_to_cell_mid_cluster_snaps() {
683 let text = "\u{4E16}"; let map = ClusterMap::from_text(text);
685
686 assert_eq!(map.byte_to_cell(0), 0);
688 assert_eq!(map.byte_to_cell(1), 0); assert_eq!(map.byte_to_cell(2), 0); }
691
692 #[test]
693 fn byte_to_entry() {
694 let text = "AB\u{4E16}C";
695 let map = ClusterMap::from_text(text);
696
697 let e = map.byte_to_entry(0).unwrap();
698 assert_eq!(e.byte_start, 0); let e = map.byte_to_entry(2).unwrap();
701 assert_eq!(e.byte_start, 2); let e = map.byte_to_entry(3).unwrap();
705 assert_eq!(e.byte_start, 2); assert!(map.byte_to_entry(100).is_none());
708 }
709
710 #[test]
715 fn cell_to_byte_ascii() {
716 let map = ClusterMap::from_text("Hello");
717 for i in 0..5 {
718 assert_eq!(map.cell_to_byte(i), i);
719 }
720 assert_eq!(map.cell_to_byte(5), 5); }
722
723 #[test]
724 fn cell_to_byte_wide() {
725 let text = "Hi\u{4E16}\u{754C}!";
726 let map = ClusterMap::from_text(text);
727
728 assert_eq!(map.cell_to_byte(0), 0); assert_eq!(map.cell_to_byte(1), 1); assert_eq!(map.cell_to_byte(2), 2); assert_eq!(map.cell_to_byte(3), 2); assert_eq!(map.cell_to_byte(4), 5); assert_eq!(map.cell_to_byte(5), 5); assert_eq!(map.cell_to_byte(6), 8); }
736
737 #[test]
738 fn cell_to_entry_continuation() {
739 let text = "\u{4E16}"; let map = ClusterMap::from_text(text);
741
742 let e0 = map.cell_to_entry(0).unwrap();
744 let e1 = map.cell_to_entry(1).unwrap();
745 assert_eq!(e0, e1);
746 assert_eq!(e0.byte_start, 0);
747 assert_eq!(e0.cell_width, 2);
748 }
749
750 #[test]
755 fn byte_range_to_cell_range_ascii() {
756 let map = ClusterMap::from_text("Hello World");
757 assert_eq!(map.byte_range_to_cell_range(0, 5), (0, 5)); assert_eq!(map.byte_range_to_cell_range(6, 11), (6, 11)); }
760
761 #[test]
762 fn byte_range_to_cell_range_wide() {
763 let text = "Hi\u{4E16}\u{754C}!"; let map = ClusterMap::from_text(text);
765
766 assert_eq!(map.byte_range_to_cell_range(2, 8), (2, 6));
768 }
769
770 #[test]
771 fn cell_range_to_byte_range_ascii() {
772 let map = ClusterMap::from_text("Hello World");
773 assert_eq!(map.cell_range_to_byte_range(0, 5), (0, 5));
774 }
775
776 #[test]
777 fn cell_range_to_byte_range_wide() {
778 let text = "Hi\u{4E16}\u{754C}!";
779 let map = ClusterMap::from_text(text);
780
781 assert_eq!(map.cell_range_to_byte_range(2, 6), (2, 8));
783
784 assert_eq!(map.cell_range_to_byte_range(3, 5), (2, 8));
786 }
787
788 #[test]
793 fn grapheme_to_cell_and_back() {
794 let text = "Hi\u{4E16}\u{754C}!";
795 let map = ClusterMap::from_text(text);
796
797 assert_eq!(map.grapheme_to_cell(0), 0); assert_eq!(map.grapheme_to_cell(2), 2); assert_eq!(map.grapheme_to_cell(4), 6); assert_eq!(map.grapheme_to_cell(5), 7); assert_eq!(map.cell_to_grapheme(0), 0); assert_eq!(map.cell_to_grapheme(2), 2); assert_eq!(map.cell_to_grapheme(3), 2); }
806
807 #[test]
808 fn grapheme_to_byte_and_back() {
809 let text = "A\u{4E16}B";
810 let map = ClusterMap::from_text(text);
811
812 assert_eq!(map.grapheme_to_byte(0), 0); assert_eq!(map.grapheme_to_byte(1), 1); assert_eq!(map.grapheme_to_byte(2), 4); assert_eq!(map.byte_to_grapheme(0), 0); assert_eq!(map.byte_to_grapheme(1), 1); assert_eq!(map.byte_to_grapheme(4), 2); }
820
821 #[test]
826 fn extract_text_for_cells_ascii() {
827 let text = "Hello World";
828 let map = ClusterMap::from_text(text);
829 assert_eq!(map.extract_text_for_cells(text, 0, 5), "Hello");
830 assert_eq!(map.extract_text_for_cells(text, 6, 11), "World");
831 }
832
833 #[test]
834 fn extract_text_for_cells_wide() {
835 let text = "Hi\u{4E16}\u{754C}!";
836 let map = ClusterMap::from_text(text);
837
838 assert_eq!(map.extract_text_for_cells(text, 2, 6), "\u{4E16}\u{754C}");
840
841 assert_eq!(map.extract_text_for_cells(text, 3, 5), "\u{4E16}\u{754C}");
843 }
844
845 #[test]
846 fn extract_text_empty_range() {
847 let text = "Hello";
848 let map = ClusterMap::from_text(text);
849 assert_eq!(map.extract_text_for_cells(text, 3, 3), "");
850 }
851
852 #[test]
857 fn roundtrip_byte_cell_byte() {
858 let texts = [
859 "Hello",
860 "\u{4E16}\u{754C}",
861 "Hi\u{4E16}\u{754C}!",
862 "e\u{0301}f",
863 "\u{05E9}\u{05DC}\u{05D5}\u{05DD}",
864 "",
865 ];
866
867 for text in texts {
868 let map = ClusterMap::from_text(text);
869
870 for entry in map.entries() {
871 let byte = entry.byte_start as usize;
872 let cell = map.byte_to_cell(byte);
873 let back = map.cell_to_byte(cell);
874 assert_eq!(
875 back, byte,
876 "Round-trip failed for text={text:?} byte={byte}"
877 );
878 }
879 }
880 }
881
882 #[test]
883 fn roundtrip_cell_byte_cell() {
884 let texts = [
885 "Hello",
886 "\u{4E16}\u{754C}",
887 "Hi\u{4E16}\u{754C}!",
888 "e\u{0301}f",
889 ];
890
891 for text in texts {
892 let map = ClusterMap::from_text(text);
893
894 for entry in map.entries() {
895 let cell = entry.cell_start as usize;
896 let byte = map.cell_to_byte(cell);
897 let back = map.byte_to_cell(byte);
898 assert_eq!(
899 back, cell,
900 "Round-trip failed for text={text:?} cell={cell}"
901 );
902 }
903 }
904 }
905
906 #[test]
911 fn monotonicity() {
912 let texts = [
913 "Hello World",
914 "Hi\u{4E16}\u{754C}! \u{05E9}\u{05DC}\u{05D5}\u{05DD}",
915 "e\u{0301}\u{0302}",
916 ];
917
918 for text in texts {
919 let map = ClusterMap::from_text(text);
920
921 for window in map.entries().windows(2) {
922 assert!(
923 window[0].byte_start < window[1].byte_start,
924 "Byte monotonicity violated: {:?}",
925 window
926 );
927 assert!(
928 window[0].cell_start < window[1].cell_start,
929 "Cell monotonicity violated: {:?}",
930 window
931 );
932 }
933 }
934 }
935
936 #[test]
941 fn contiguity() {
942 let text = "Hi\u{4E16}\u{754C}!";
943 let map = ClusterMap::from_text(text);
944
945 for window in map.entries().windows(2) {
947 assert_eq!(
948 window[0].byte_end, window[1].byte_start,
949 "Byte gap: {:?}",
950 window
951 );
952 }
953
954 for window in map.entries().windows(2) {
956 assert_eq!(
957 window[0].cell_end(),
958 window[1].cell_start,
959 "Cell gap: {:?}",
960 window
961 );
962 }
963
964 assert_eq!(map.entries()[0].byte_start, 0);
966 assert_eq!(map.entries()[0].cell_start, 0);
967
968 let last = map.entries().last().unwrap();
970 assert_eq!(last.byte_end, map.total_bytes() as u32);
971 assert_eq!(last.cell_end(), map.total_cells() as u32);
972 }
973
974 #[test]
979 fn from_shaped_run_noop() {
980 use crate::script_segmentation::{RunDirection, Script};
981 use crate::shaping::{FontFeatures, NoopShaper, TextShaper};
982
983 let text = "Hi\u{4E16}!";
984 let shaper = NoopShaper;
985 let ff = FontFeatures::default();
986 let run = shaper.shape(text, Script::Latin, RunDirection::Ltr, &ff);
987
988 let map = ClusterMap::from_shaped_run(text, &run);
989
990 let text_map = ClusterMap::from_text(text);
992
993 assert_eq!(map.cluster_count(), text_map.cluster_count());
994 assert_eq!(map.total_cells(), text_map.total_cells());
995 assert_eq!(map.total_bytes(), text_map.total_bytes());
996 }
997
998 #[test]
999 fn from_shaped_run_empty() {
1000 use crate::shaping::ShapedRun;
1001
1002 let map = ClusterMap::from_shaped_run(
1003 "",
1004 &ShapedRun {
1005 glyphs: vec![],
1006 total_advance: 0,
1007 },
1008 );
1009 assert!(map.is_empty());
1010 }
1011
1012 #[test]
1013 fn from_shaped_run_ligature_cluster_boundaries() {
1014 use crate::shaping::{ShapedGlyph, ShapedRun};
1015
1016 let text = "file";
1017 let run = ShapedRun {
1018 glyphs: vec![
1019 ShapedGlyph {
1020 glyph_id: 1,
1021 cluster: 0, x_advance: 2,
1023 y_advance: 0,
1024 x_offset: 0,
1025 y_offset: 0,
1026 },
1027 ShapedGlyph {
1028 glyph_id: 2,
1029 cluster: 2,
1030 x_advance: 1,
1031 y_advance: 0,
1032 x_offset: 0,
1033 y_offset: 0,
1034 },
1035 ShapedGlyph {
1036 glyph_id: 3,
1037 cluster: 3,
1038 x_advance: 1,
1039 y_advance: 0,
1040 x_offset: 0,
1041 y_offset: 0,
1042 },
1043 ],
1044 total_advance: 4,
1045 };
1046
1047 let map = ClusterMap::from_shaped_run(text, &run);
1048 assert_eq!(map.cluster_count(), 3);
1049 assert_eq!(map.total_cells(), 4);
1050
1051 assert_eq!(map.byte_to_cell(0), 0);
1053 assert_eq!(map.byte_to_cell(1), 0);
1054 assert_eq!(map.byte_to_cell(2), 2);
1055
1056 assert_eq!(map.cell_to_byte(1), 0);
1058 assert_eq!(map.cell_to_byte(2), 2);
1059
1060 assert_eq!(map.extract_text_for_cells(text, 0, 2), "fi");
1062 }
1063}