1use std::fmt;
2use unicode_width::UnicodeWidthStr;
3
4#[derive(Debug, Clone)]
10pub struct RenderedBlock {
11 cells: Vec<Vec<String>>,
13 width: usize,
15 height: usize,
17 baseline: usize,
19}
20
21impl RenderedBlock {
22 pub fn new(cells: Vec<Vec<String>>, baseline: usize) -> Self {
27 let height = cells.len();
28 let width = cells.first().map_or(0, |row| {
29 row.iter().map(|c| UnicodeWidthStr::width(c.as_str())).sum()
30 });
31 Self {
32 cells,
33 width,
34 height,
35 baseline,
36 }
37 }
38
39 pub fn from_char(ch: char) -> Self {
41 let s = ch.to_string();
42 let width = UnicodeWidthStr::width(s.as_str()).max(1);
43 Self {
44 cells: vec![vec![s]],
45 width,
46 height: 1,
47 baseline: 0,
48 }
49 }
50
51 pub fn from_text(text: &str) -> Self {
53 if text.is_empty() {
54 return Self::empty();
55 }
56 let cells: Vec<String> = text.chars().map(|c| c.to_string()).collect();
57 let width = UnicodeWidthStr::width(text);
58 Self {
59 cells: vec![cells],
60 width,
61 height: 1,
62 baseline: 0,
63 }
64 }
65
66 pub fn empty() -> Self {
68 Self {
69 cells: vec![],
70 width: 0,
71 height: 0,
72 baseline: 0,
73 }
74 }
75
76 pub fn width(&self) -> usize {
77 self.width
78 }
79
80 pub fn height(&self) -> usize {
81 self.height
82 }
83
84 pub fn baseline(&self) -> usize {
85 self.baseline
86 }
87
88 pub fn cells(&self) -> &[Vec<String>] {
89 &self.cells
90 }
91
92 pub fn is_empty(&self) -> bool {
93 self.height == 0 || self.width == 0
94 }
95
96 pub fn beside(&self, other: &RenderedBlock) -> RenderedBlock {
99 if self.is_empty() {
100 return other.clone();
101 }
102 if other.is_empty() {
103 return self.clone();
104 }
105
106 let baseline = self.baseline.max(other.baseline);
107 let above_baseline = baseline;
108
109 let self_below = self.height.saturating_sub(self.baseline + 1);
110 let other_below = other.height.saturating_sub(other.baseline + 1);
111 let below_baseline = self_below.max(other_below);
112
113 let total_height = above_baseline + 1 + below_baseline;
114 let total_width = self.width + other.width;
115
116 let self_top_pad = above_baseline - self.baseline;
117 let other_top_pad = above_baseline - other.baseline;
118
119 let mut rows = Vec::with_capacity(total_height);
120 for row_idx in 0..total_height {
121 let mut row = Vec::new();
122
123 let self_row = row_idx.checked_sub(self_top_pad);
125 if let Some(sr) = self_row {
126 if sr < self.height {
127 row.extend(self.cells[sr].iter().cloned());
128 } else {
129 row.extend(std::iter::repeat_n(" ".to_string(), self.width));
130 }
131 } else {
132 row.extend(std::iter::repeat_n(" ".to_string(), self.width));
133 }
134
135 let other_row = row_idx.checked_sub(other_top_pad);
137 if let Some(or_idx) = other_row {
138 if or_idx < other.height {
139 row.extend(other.cells[or_idx].iter().cloned());
140 } else {
141 row.extend(std::iter::repeat_n(" ".to_string(), other.width));
142 }
143 } else {
144 row.extend(std::iter::repeat_n(" ".to_string(), other.width));
145 }
146
147 rows.push(row);
148 }
149
150 RenderedBlock {
151 cells: rows,
152 width: total_width,
153 height: total_height,
154 baseline,
155 }
156 }
157
158 pub fn above(
161 top: &RenderedBlock,
162 bottom: &RenderedBlock,
163 baseline_row: usize,
164 ) -> RenderedBlock {
165 let width = top.width.max(bottom.width);
166 let mut rows = Vec::with_capacity(top.height + bottom.height);
167
168 for r in 0..top.height {
169 rows.push(Self::pad_row_to_width(&top.cells[r], top.width, width));
170 }
171 for r in 0..bottom.height {
172 rows.push(Self::pad_row_to_width(
173 &bottom.cells[r],
174 bottom.width,
175 width,
176 ));
177 }
178
179 RenderedBlock {
180 cells: rows,
181 width,
182 height: top.height + bottom.height,
183 baseline: baseline_row,
184 }
185 }
186
187 pub fn pad(&self, left: usize, right: usize, top: usize, bottom: usize) -> RenderedBlock {
189 let new_width = left + self.width + right;
190 let new_height = top + self.height + bottom;
191
192 let mut rows = Vec::with_capacity(new_height);
193
194 for _ in 0..top {
196 rows.push(vec![" ".to_string(); new_width]);
197 }
198
199 for r in 0..self.height {
201 let mut row = Vec::with_capacity(new_width);
202 row.extend(std::iter::repeat_n(" ".to_string(), left));
203 row.extend(self.cells[r].iter().cloned());
204 row.extend(std::iter::repeat_n(" ".to_string(), right));
205 rows.push(row);
206 }
207
208 for _ in 0..bottom {
210 rows.push(vec![" ".to_string(); new_width]);
211 }
212
213 RenderedBlock {
214 cells: rows,
215 width: new_width,
216 height: new_height,
217 baseline: self.baseline + top,
218 }
219 }
220
221 pub fn center_in(&self, target_width: usize) -> RenderedBlock {
223 if target_width <= self.width {
224 return self.clone();
225 }
226 let total_pad = target_width - self.width;
227 let left_pad = total_pad / 2;
228 let right_pad = total_pad - left_pad;
229 self.pad(left_pad, right_pad, 0, 0)
230 }
231
232 fn pad_row_to_width(row: &[String], current_width: usize, target_width: usize) -> Vec<String> {
234 let mut result = row.to_vec();
235 let pad = target_width.saturating_sub(current_width);
236 result.extend(std::iter::repeat_n(" ".to_string(), pad));
237 result
238 }
239
240 pub fn hline(ch: char, width: usize) -> RenderedBlock {
242 let cells = vec![vec![ch.to_string(); width]];
243 RenderedBlock {
244 cells,
245 width,
246 height: 1,
247 baseline: 0,
248 }
249 }
250}
251
252impl fmt::Display for RenderedBlock {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 for (i, row) in self.cells.iter().enumerate() {
255 if i > 0 {
256 writeln!(f)?;
257 }
258 for cell in row {
259 write!(f, "{}", cell)?;
260 }
261 }
262 Ok(())
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_from_char() {
272 let block = RenderedBlock::from_char('x');
273 assert_eq!(block.width(), 1);
274 assert_eq!(block.height(), 1);
275 assert_eq!(block.baseline(), 0);
276 assert_eq!(format!("{}", block), "x");
277 }
278
279 #[test]
280 fn test_from_text() {
281 let block = RenderedBlock::from_text("hello");
282 assert_eq!(block.width(), 5);
283 assert_eq!(block.height(), 1);
284 assert_eq!(format!("{}", block), "hello");
285 }
286
287 #[test]
288 fn test_beside_baseline_aligned() {
289 let a = RenderedBlock::from_text("ab");
291 let b = RenderedBlock::from_text("cd");
292 let result = a.beside(&b);
293 assert_eq!(result.width(), 4);
294 assert_eq!(result.height(), 1);
295 assert_eq!(format!("{}", result), "abcd");
296 }
297
298 #[test]
299 fn test_beside_different_heights() {
300 let a = RenderedBlock::new(
302 vec![vec!["a".into()], vec!["b".into()], vec!["c".into()]],
303 1,
304 );
305 let d = RenderedBlock::from_char('d');
307 let result = a.beside(&d);
308 assert_eq!(result.height(), 3);
309 assert_eq!(result.baseline(), 1);
310 let output = format!("{}", result);
312 let lines: Vec<&str> = output.lines().collect();
313 assert_eq!(lines[0], "a ");
314 assert_eq!(lines[1], "bd");
315 assert_eq!(lines[2], "c ");
316 }
317
318 #[test]
319 fn test_center_in() {
320 let block = RenderedBlock::from_text("ab");
321 let centered = block.center_in(6);
322 assert_eq!(centered.width(), 6);
323 assert_eq!(format!("{}", centered), " ab ");
324 }
325
326 #[test]
327 fn test_above() {
328 let top = RenderedBlock::from_text("abc");
329 let bottom = RenderedBlock::from_text("de");
330 let result = RenderedBlock::above(&top, &bottom, 0);
331 assert_eq!(result.height(), 2);
332 assert_eq!(result.width(), 3);
333 let output = format!("{}", result);
334 let lines: Vec<&str> = output.lines().collect();
335 assert_eq!(lines[0], "abc");
336 assert_eq!(lines[1], "de ");
337 }
338
339 #[test]
340 fn test_pad() {
341 let block = RenderedBlock::from_char('x');
342 let padded = block.pad(1, 1, 1, 1);
343 assert_eq!(padded.width(), 3);
344 assert_eq!(padded.height(), 3);
345 assert_eq!(padded.baseline(), 1);
346 let output = format!("{}", padded);
347 let lines: Vec<&str> = output.lines().collect();
348 assert_eq!(lines[0], " ");
349 assert_eq!(lines[1], " x ");
350 assert_eq!(lines[2], " ");
351 }
352}