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