1use unicode_segmentation::UnicodeSegmentation;
11use unicode_width::UnicodeWidthStr;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct GraphemeSpan {
16 pub byte: usize,
18 pub cell: u16,
20 pub width: u8,
23}
24
25#[derive(Debug, Clone, Default)]
27pub struct LineLayout {
28 spans: Vec<GraphemeSpan>,
29 pub len_bytes: usize,
31 pub width: u16,
33}
34
35impl LineLayout {
36 pub fn build(text: &str, tab: u16) -> Self {
39 let tab = tab.max(1);
40 let mut spans = Vec::with_capacity(text.len() / 2 + 4);
41 let mut cell: u16 = 0;
42 for (byte, g) in text.grapheme_indices(true) {
43 let w = if g == "\t" {
44 (tab - cell % tab) as u8
45 } else {
46 UnicodeWidthStr::width(g).min(u8::MAX as usize) as u8
47 };
48 spans.push(GraphemeSpan {
49 byte,
50 cell,
51 width: w,
52 });
53 cell = cell.saturating_add(w as u16);
54 }
55 LineLayout {
56 spans,
57 len_bytes: text.len(),
58 width: cell,
59 }
60 }
61
62 pub fn spans(&self) -> &[GraphemeSpan] {
64 &self.spans
65 }
66
67 pub fn cell_at_byte(&self, byte: usize) -> u16 {
71 if byte >= self.len_bytes {
72 return self.width;
73 }
74 self.spans
75 .iter()
76 .rev()
77 .find(|s| s.byte <= byte)
78 .map(|s| s.cell)
79 .unwrap_or(0)
80 }
81
82 pub fn byte_at_cell(&self, cell: u16) -> usize {
86 match self.spans.iter().rev().find(|s| s.cell <= cell) {
87 Some(s) => s.byte,
88 None => 0,
89 }
90 }
91
92 pub fn is_ascii_fast(&self) -> bool {
95 self.spans.iter().all(|s| s.width == 1)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn ascii_is_identity() {
105 let l = LineLayout::build("hello", 8);
106 assert_eq!(l.width, 5);
107 assert_eq!(l.cell_at_byte(3), 3);
108 assert_eq!(l.byte_at_cell(3), 3);
109 assert!(l.is_ascii_fast());
110 }
111
112 #[test]
113 fn cjk_is_two_cells() {
114 let l = LineLayout::build("a界b", 8);
115 assert_eq!(l.width, 4);
116 assert_eq!(l.cell_at_byte(1), 1); assert_eq!(l.cell_at_byte(4), 3); assert_eq!(l.byte_at_cell(3), 4);
119 }
120
121 #[test]
122 fn emoji_cluster_is_one_unit() {
123 let l = LineLayout::build("x\u{1F9D1}\u{200D}\u{1F680}y", 8); assert_eq!(l.spans().len(), 3);
125 assert_eq!(l.spans()[1].width, 2);
126 assert_eq!(l.width, 4);
127 }
128
129 #[test]
130 fn tab_expands_to_its_stop() {
131 let l = LineLayout::build("ab\tc", 4);
132 assert_eq!(l.spans()[2].width, 2); assert_eq!(l.cell_at_byte(3), 4); assert_eq!(l.width, 5);
135 }
136}