1use unicode_width::UnicodeWidthChar;
2use unicode_width::UnicodeWidthStr;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Row {
7 pub text: String,
8 pub explicit_break: bool,
10}
11
12impl Row {
13 pub fn width(&self) -> usize {
14 self.text.width()
15 }
16}
17
18pub struct RowBuilder {
22 target_width: usize,
23 current_line: String,
25 rows: Vec<Row>,
27}
28
29impl RowBuilder {
30 pub fn new(target_width: usize) -> Self {
31 Self {
32 target_width: target_width.max(1),
33 current_line: String::new(),
34 rows: Vec::new(),
35 }
36 }
37
38 pub const fn width(&self) -> usize {
39 self.target_width
40 }
41
42 pub fn set_width(&mut self, width: usize) {
43 self.target_width = width.max(1);
44 let mut all = String::new();
46 for row in self.rows.drain(..) {
47 all.push_str(&row.text);
48 if row.explicit_break {
49 all.push('\n');
50 }
51 }
52 all.push_str(&self.current_line);
53 self.current_line.clear();
54 self.push_fragment(&all);
55 }
56
57 pub fn push_fragment(&mut self, fragment: &str) {
59 if fragment.is_empty() {
60 return;
61 }
62 let mut start = 0usize;
63 for (i, ch) in fragment.char_indices() {
64 if ch == '\n' {
65 if start < i {
67 self.current_line.push_str(&fragment[start..i]);
68 }
69 self.flush_current_line(true);
70 start = i + ch.len_utf8();
71 }
72 }
73 if start < fragment.len() {
74 self.current_line.push_str(&fragment[start..]);
75 self.wrap_current_line();
76 }
77 }
78
79 pub fn end_line(&mut self) {
81 self.flush_current_line(true);
82 }
83
84 pub fn drain_rows(&mut self) -> Vec<Row> {
86 std::mem::take(&mut self.rows)
87 }
88
89 pub fn rows(&self) -> &[Row] {
91 &self.rows
92 }
93
94 pub fn display_rows(&self) -> Vec<Row> {
96 let mut out = self.rows.clone();
97 if !self.current_line.is_empty() {
98 out.push(Row {
99 text: self.current_line.clone(),
100 explicit_break: false,
101 });
102 }
103 out
104 }
105
106 pub fn drain_commit_ready(&mut self, max_keep: usize) -> Vec<Row> {
109 let display_count = self.rows.len() + if self.current_line.is_empty() { 0 } else { 1 };
110 if display_count <= max_keep {
111 return Vec::new();
112 }
113 let to_commit = display_count - max_keep;
114 let commit_count = to_commit.min(self.rows.len());
115 let mut drained = Vec::with_capacity(commit_count);
116 for _ in 0..commit_count {
117 drained.push(self.rows.remove(0));
118 }
119 drained
120 }
121
122 fn flush_current_line(&mut self, explicit_break: bool) {
123 self.wrap_current_line();
125 if explicit_break {
128 if self.current_line.is_empty() {
129 self.rows.push(Row {
131 text: String::new(),
132 explicit_break: true,
133 });
134 } else {
135 let mut s = String::new();
137 std::mem::swap(&mut s, &mut self.current_line);
138 self.rows.push(Row {
139 text: s,
140 explicit_break: true,
141 });
142 }
143 }
144 self.current_line.clear();
146 }
147
148 fn wrap_current_line(&mut self) {
149 loop {
151 if self.current_line.is_empty() {
152 break;
153 }
154 let (prefix, suffix, taken) =
155 take_prefix_by_width(&self.current_line, self.target_width);
156 if taken == 0 {
157 if let Some((i, ch)) = self.current_line.char_indices().next() {
159 let len = i + ch.len_utf8();
160 let p = self.current_line[..len].to_string();
161 self.rows.push(Row {
162 text: p,
163 explicit_break: false,
164 });
165 self.current_line = self.current_line[len..].to_string();
166 continue;
167 }
168 break;
169 }
170 if suffix.is_empty() {
171 break;
173 } else {
174 self.rows.push(Row {
176 text: prefix,
177 explicit_break: false,
178 });
179 self.current_line = suffix.to_string();
180 }
181 }
182 }
183}
184
185pub fn take_prefix_by_width(text: &str, max_cols: usize) -> (String, &str, usize) {
188 if max_cols == 0 || text.is_empty() {
189 return (String::new(), text, 0);
190 }
191 let mut cols = 0usize;
192 let mut end_idx = 0usize;
193 for (i, ch) in text.char_indices() {
194 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
195 if cols.saturating_add(ch_width) > max_cols {
196 break;
197 }
198 cols += ch_width;
199 end_idx = i + ch.len_utf8();
200 if cols == max_cols {
201 break;
202 }
203 }
204 let prefix = text[..end_idx].to_string();
205 let suffix = &text[end_idx..];
206 (prefix, suffix, cols)
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use pretty_assertions::assert_eq;
213
214 #[test]
215 fn rows_do_not_exceed_width_ascii() {
216 let mut rb = RowBuilder::new(10);
217 rb.push_fragment("hello whirl this is a test");
218 let rows = rb.rows().to_vec();
219 assert_eq!(
220 rows,
221 vec![
222 Row {
223 text: "hello whir".to_string(),
224 explicit_break: false
225 },
226 Row {
227 text: "l this is ".to_string(),
228 explicit_break: false
229 }
230 ]
231 );
232 }
233
234 #[test]
235 fn rows_do_not_exceed_width_emoji_cjk() {
236 let mut rb = RowBuilder::new(6);
238 rb.push_fragment("😀😀 ä½ å¥½");
239 let rows = rb.rows().to_vec();
240 assert_eq!(
244 rows,
245 vec![Row {
246 text: "😀😀 ".to_string(),
247 explicit_break: false
248 }]
249 );
250 }
251
252 #[test]
253 fn fragmentation_invariance_long_token() {
254 let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let mut rb_all = RowBuilder::new(7);
256 rb_all.push_fragment(s);
257 let all_rows = rb_all.rows().to_vec();
258
259 let mut rb_chunks = RowBuilder::new(7);
260 for i in (0..s.len()).step_by(3) {
261 let end = (i + 3).min(s.len());
262 rb_chunks.push_fragment(&s[i..end]);
263 }
264 let chunk_rows = rb_chunks.rows().to_vec();
265
266 assert_eq!(all_rows, chunk_rows);
267 }
268
269 #[test]
270 fn newline_splits_rows() {
271 let mut rb = RowBuilder::new(10);
272 rb.push_fragment("hello\nworld");
273 let rows = rb.display_rows();
274 assert!(rows.iter().any(|r| r.explicit_break));
275 assert_eq!(rows[0].text, "hello");
276 assert!(rows.iter().any(|r| r.text.starts_with("world")));
278 }
279
280 #[test]
281 fn rewrap_on_width_change() {
282 let mut rb = RowBuilder::new(10);
283 rb.push_fragment("abcdefghijK");
284 assert!(!rb.rows().is_empty());
285 rb.set_width(5);
286 for r in rb.rows() {
287 assert!(r.width() <= 5);
288 }
289 }
290}