1use crate::style::Color;
7
8#[derive(Clone, PartialEq, Eq, Debug)]
10pub struct Cell {
11 pub ch: char,
12 pub combining: String,
13 pub fg: Option<Color>,
14 pub bg: Option<Color>,
15 pub bold: bool,
16 pub italic: bool,
17 pub underline: bool,
18 pub reverse: bool,
19 pub dim: bool,
20 pub strikethrough: bool,
21}
22
23static EMPTY_CELL: Cell = Cell {
24 ch: ' ',
25 combining: String::new(),
26 fg: None,
27 bg: None,
28 bold: false,
29 italic: false,
30 underline: false,
31 reverse: false,
32 dim: false,
33 strikethrough: false,
34};
35
36const WIDE_CONTINUATION: char = '\0';
37
38impl Default for Cell {
39 fn default() -> Self {
40 EMPTY_CELL.clone()
41 }
42}
43
44impl Cell {
45 pub fn with_char(ch: char) -> Self {
46 Self {
47 ch,
48 ..Default::default()
49 }
50 }
51
52 pub fn styled(ch: char, style: &CellStyle) -> Self {
53 Self {
54 ch,
55 combining: String::new(),
56 fg: style.fg,
57 bg: style.bg,
58 bold: style.bold,
59 italic: style.italic,
60 underline: style.underline,
61 reverse: style.reverse,
62 dim: style.dim,
63 strikethrough: style.strikethrough,
64 }
65 }
66
67 pub fn to_ansi(&self) -> String {
68 if self.ch == WIDE_CONTINUATION {
69 return String::new();
70 }
71
72 use std::fmt::Write;
73
74 let has_style = self.bold
75 || self.dim
76 || self.italic
77 || self.underline
78 || self.reverse
79 || self.strikethrough
80 || self.fg.is_some()
81 || self.bg.is_some();
82
83 if !has_style {
84 let mut out = self.ch.to_string();
85 out.push_str(&self.combining);
86 return out;
87 }
88
89 let mut out = String::with_capacity(24);
90 out.push_str("\x1b[");
91 let mut first = true;
92
93 macro_rules! push_code {
94 ($code:expr) => {
95 if !first {
96 out.push(';');
97 }
98 let _ = write!(out, "{}", $code);
99 #[allow(unused_assignments)]
100 {
101 first = false;
102 }
103 };
104 }
105
106 if self.bold {
107 push_code!("1");
108 }
109 if self.dim {
110 push_code!("2");
111 }
112 if self.italic {
113 push_code!("3");
114 }
115 if self.underline {
116 push_code!("4");
117 }
118 if self.reverse {
119 push_code!("7");
120 }
121 if self.strikethrough {
122 push_code!("9");
123 }
124 if let Some(ref c) = self.fg {
125 push_code!(c.fg_ansi());
126 }
127 if let Some(ref c) = self.bg {
128 push_code!(c.bg_ansi());
129 }
130
131 out.push('m');
132 out.push(self.ch);
133 out.push_str(&self.combining);
134 out.push_str("\x1b[0m");
135 out
136 }
137
138 pub fn write_ansi(&self, buf: &mut String) {
140 if self.ch == WIDE_CONTINUATION {
141 return;
142 }
143
144 use std::fmt::Write;
145
146 let has_style = self.bold
147 || self.dim
148 || self.italic
149 || self.underline
150 || self.reverse
151 || self.strikethrough
152 || self.fg.is_some()
153 || self.bg.is_some();
154
155 if !has_style {
156 buf.push(self.ch);
157 buf.push_str(&self.combining);
158 return;
159 }
160
161 buf.push_str("\x1b[");
162 let mut first = true;
163
164 macro_rules! push_code {
165 ($code:expr) => {
166 if !first {
167 buf.push(';');
168 }
169 let _ = write!(buf, "{}", $code);
170 #[allow(unused_assignments)]
171 {
172 first = false;
173 }
174 };
175 }
176
177 if self.bold {
178 push_code!("1");
179 }
180 if self.dim {
181 push_code!("2");
182 }
183 if self.italic {
184 push_code!("3");
185 }
186 if self.underline {
187 push_code!("4");
188 }
189 if self.reverse {
190 push_code!("7");
191 }
192 if self.strikethrough {
193 push_code!("9");
194 }
195 if let Some(ref c) = self.fg {
196 push_code!(c.fg_ansi());
197 }
198 if let Some(ref c) = self.bg {
199 push_code!(c.bg_ansi());
200 }
201
202 buf.push('m');
203 buf.push(self.ch);
204 buf.push_str(&self.combining);
205 buf.push_str("\x1b[0m");
206 }
207}
208
209#[derive(Clone, Debug, Default)]
210pub struct CellStyle {
211 pub fg: Option<Color>,
212 pub bg: Option<Color>,
213 pub bold: bool,
214 pub italic: bool,
215 pub underline: bool,
216 pub reverse: bool,
217 pub dim: bool,
218 pub strikethrough: bool,
219}
220
221pub struct Grid {
222 pub cells: Vec<Vec<Cell>>,
223 pub width: u16,
224 pub height: u16,
225}
226
227impl Grid {
228 pub fn new(width: u16, height: u16) -> Self {
229 let cells = vec![vec![Cell::default(); width as usize]; height as usize];
230 Self {
231 cells,
232 width,
233 height,
234 }
235 }
236
237 pub fn get(&self, x: u16, y: u16) -> &Cell {
238 self.try_get(x, y).unwrap_or(&EMPTY_CELL)
239 }
240
241 pub fn try_get(&self, x: u16, y: u16) -> Option<&Cell> {
242 self.cells
243 .get(y as usize)
244 .and_then(|row| row.get(x as usize))
245 }
246
247 pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
248 if x < self.width && y < self.height {
249 let row = y as usize;
250 let col = x as usize;
251 let width = unicode_width::UnicodeWidthChar::width(cell.ch).unwrap_or(1);
252 if width == 0 || col.saturating_add(width) > self.width as usize {
253 return;
254 }
255 for target_col in col..col.saturating_add(width).max(col + 1) {
256 self.clear_wide_span_at(row, target_col);
257 }
258 self.cells[row][col] = cell.clone();
259 for offset in 1..width {
260 let mut continuation = cell.clone();
261 continuation.ch = WIDE_CONTINUATION;
262 continuation.combining.clear();
263 self.cells[row][col + offset] = continuation;
264 }
265 }
266 }
267
268 pub fn write_str(&mut self, x: u16, y: u16, text: &str, style: &CellStyle) {
269 let mut col = x as usize;
270 let row = y as usize;
271 if row >= self.height as usize {
272 return;
273 }
274 let mut last_base_col: Option<usize> = None;
275 for ch in text.chars() {
276 let width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
277 if width == 0 {
278 if let Some(base_col) =
279 last_base_col.or_else(|| self.leading_combining_base_col(row, col))
280 {
281 self.cells[row][base_col].combining.push(ch);
282 }
283 continue;
284 }
285 if col.saturating_add(width) > self.width as usize {
286 break;
287 }
288 for target_col in col..col.saturating_add(width).max(col + 1) {
289 self.clear_wide_span_at(row, target_col);
290 }
291 self.cells[row][col] = Cell::styled(ch, style);
292 for offset in 1..width {
293 self.cells[row][col + offset] = Cell::styled(WIDE_CONTINUATION, style);
294 }
295 last_base_col = Some(col);
296 col += width;
297 }
298 }
299
300 fn leading_combining_base_col(&self, row: usize, col: usize) -> Option<usize> {
301 if col == 0 || col >= self.width as usize {
302 return None;
303 }
304
305 let (start, _) = self.wide_span_bounds_at(row, col - 1);
306 let cell = self.cells.get(row)?.get(start)?;
307 if cell.ch == ' ' || cell.ch == WIDE_CONTINUATION {
308 return None;
309 }
310
311 Some(start)
312 }
313
314 fn clear_wide_span_at(&mut self, row: usize, col: usize) {
315 if row >= self.height as usize || col >= self.width as usize {
316 return;
317 }
318
319 let row_cells = &mut self.cells[row];
320 let mut start = col;
321 if row_cells[col].ch == WIDE_CONTINUATION {
322 while start > 0 && row_cells[start].ch == WIDE_CONTINUATION {
323 start -= 1;
324 }
325 }
326
327 let width = unicode_width::UnicodeWidthChar::width(row_cells[start].ch).unwrap_or(1);
328 if width <= 1 || start.saturating_add(width) <= col {
329 row_cells[col] = Cell::default();
330 return;
331 }
332
333 let end = start.saturating_add(width).min(row_cells.len());
334 for cell in row_cells.iter_mut().take(end).skip(start) {
335 *cell = Cell::default();
336 }
337 }
338
339 fn wide_span_bounds_at(&self, row: usize, col: usize) -> (usize, usize) {
340 let Some(row_cells) = self.cells.get(row) else {
341 return (col, col);
342 };
343 if col >= row_cells.len() {
344 return (col, col);
345 }
346
347 let mut start = col;
348 if row_cells[col].ch == WIDE_CONTINUATION {
349 while start > 0 && row_cells[start].ch == WIDE_CONTINUATION {
350 start -= 1;
351 }
352 }
353
354 let width = unicode_width::UnicodeWidthChar::width(row_cells[start].ch).unwrap_or(1);
355 if width <= 1 || start.saturating_add(width) <= col {
356 return (col, col.saturating_add(1).min(row_cells.len()));
357 }
358
359 (start, start.saturating_add(width).min(row_cells.len()))
360 }
361
362 pub fn fill_bg(&mut self, x: u16, y: u16, w: u16, h: u16, color: Color) {
363 let end_y = y.saturating_add(h).min(self.height);
364 let end_x = x.saturating_add(w).min(self.width);
365 for row in y as usize..end_y as usize {
366 let mut col = x as usize;
367 while col < end_x as usize {
368 let (span_start, span_end) = self.wide_span_bounds_at(row, col);
369 for target_col in span_start..span_end {
370 self.cells[row][target_col].bg = Some(color);
371 }
372 col = span_end.max(col.saturating_add(1));
373 }
374 }
375 }
376
377 pub fn diff(&self, other: &Grid) -> Vec<CellChange> {
378 let mut changes = Vec::new();
379 let max_h = self.height.min(other.height);
380 let max_w = self.width.min(other.width);
381
382 for y in 0..max_h {
383 for x in 0..max_w {
384 let old = &self.cells[y as usize][x as usize];
385 let new = &other.cells[y as usize][x as usize];
386 if old != new {
387 changes.push(CellChange {
388 x,
389 y,
390 cell: new.clone(),
391 });
392 }
393 }
394
395 if other.width > self.width {
396 for x in self.width..other.width {
397 let cell = &other.cells[y as usize][x as usize];
398 if *cell != Cell::default() {
399 changes.push(CellChange {
400 x,
401 y,
402 cell: cell.clone(),
403 });
404 }
405 }
406 }
407 }
408
409 if other.height > self.height {
410 for y in self.height..other.height {
411 for x in 0..other.width {
412 let cell = &other.cells[y as usize][x as usize];
413 if *cell != Cell::default() {
414 changes.push(CellChange {
415 x,
416 y,
417 cell: cell.clone(),
418 });
419 }
420 }
421 }
422 }
423
424 changes
425 }
426
427 pub fn render_to_string(&self) -> String {
429 let mut output = String::with_capacity((self.width as usize * self.height as usize) * 4);
430 for y in 0..self.height {
431 if y > 0 {
432 output.push('\n');
433 }
434 for x in 0..self.width {
435 self.cells[y as usize][x as usize].write_ansi(&mut output);
436 }
437 }
438 output
439 }
440}
441
442#[derive(Debug)]
443pub struct CellChange {
444 pub x: u16,
445 pub y: u16,
446 pub cell: Cell,
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn grid_new_creates_empty_cells() {
455 let grid = Grid::new(10, 5);
456 assert_eq!(grid.width, 10);
457 assert_eq!(grid.height, 5);
458 assert_eq!(grid.cells.len(), 5);
459 assert_eq!(grid.cells[0].len(), 10);
460 assert_eq!(grid.get(0, 0).ch, ' ');
461 }
462
463 #[test]
464 fn grid_set_and_get() {
465 let mut grid = Grid::new(10, 5);
466 let cell = Cell::with_char('X');
467 grid.set(3, 2, cell.clone());
468 assert_eq!(grid.get(3, 2).ch, 'X');
469 assert_eq!(grid.try_get(3, 2), Some(&cell));
470 }
471
472 #[test]
473 fn grid_get_out_of_bounds_returns_empty_cell() {
474 let grid = Grid::new(2, 1);
475 assert!(grid.try_get(2, 0).is_none());
476 assert!(grid.try_get(0, 1).is_none());
477 assert_eq!(grid.get(2, 0), &Cell::default());
478 assert_eq!(grid.get(0, 1), &Cell::default());
479 }
480
481 #[test]
482 fn grid_set_out_of_bounds_is_noop() {
483 let mut grid = Grid::new(10, 5);
484 grid.set(15, 2, Cell::with_char('X'));
485 grid.set(3, 10, Cell::with_char('Y'));
486 }
487
488 #[test]
489 fn grid_write_str() {
490 let mut grid = Grid::new(20, 5);
491 let style = CellStyle::default();
492 grid.write_str(2, 1, "hello", &style);
493 assert_eq!(grid.get(2, 1).ch, 'h');
494 assert_eq!(grid.get(3, 1).ch, 'e');
495 assert_eq!(grid.get(4, 1).ch, 'l');
496 assert_eq!(grid.get(5, 1).ch, 'l');
497 assert_eq!(grid.get(6, 1).ch, 'o');
498 }
499
500 #[test]
501 fn grid_write_str_clips_at_boundary() {
502 let mut grid = Grid::new(5, 1);
503 let style = CellStyle::default();
504 grid.write_str(3, 0, "hello", &style);
505 assert_eq!(grid.get(3, 0).ch, 'h');
506 assert_eq!(grid.get(4, 0).ch, 'e');
507 }
508
509 #[test]
510 fn grid_write_str_drops_wide_char_that_would_overflow() {
511 let mut grid = Grid::new(4, 1);
512 let style = CellStyle::default();
513
514 grid.write_str(3, 0, "界", &style);
515
516 assert_eq!(grid.get(3, 0).ch, ' ');
517 }
518
519 #[test]
520 fn grid_write_str_keeps_zero_width_marks_with_base_glyph() {
521 let mut grid = Grid::new(2, 1);
522 let style = CellStyle::default();
523
524 grid.write_str(0, 0, "ab\u{0301}", &style);
525 grid.write_str(2, 0, "\u{0301}", &style);
526
527 assert_eq!(grid.get(1, 0).ch, 'b');
528 assert_eq!(grid.get(1, 0).combining, "\u{0301}");
529 assert_eq!(grid.render_to_string(), "ab\u{0301}");
530 assert_eq!(crate::style::visible_len(&grid.render_to_string()), 2);
531 }
532
533 #[test]
534 fn grid_write_str_attaches_leading_zero_width_mark_to_previous_cell() {
535 let mut grid = Grid::new(2, 1);
536 let style = CellStyle::default();
537
538 grid.write_str(0, 0, "a", &style);
539 grid.write_str(1, 0, "\u{0301}", &style);
540
541 assert_eq!(grid.get(0, 0).ch, 'a');
542 assert_eq!(grid.get(0, 0).combining, "\u{0301}");
543 assert_eq!(grid.render_to_string(), "a\u{0301} ");
544 }
545
546 #[test]
547 fn grid_write_str_attaches_leading_zero_width_mark_to_previous_wide_cell() {
548 let mut grid = Grid::new(3, 1);
549 let style = CellStyle::default();
550
551 grid.write_str(0, 0, "界", &style);
552 grid.write_str(2, 0, "\u{0301}", &style);
553
554 assert_eq!(grid.get(0, 0).ch, '界');
555 assert_eq!(grid.get(0, 0).combining, "\u{0301}");
556 assert_eq!(grid.get(1, 0).ch, WIDE_CONTINUATION);
557 assert_eq!(crate::style::visible_len(&grid.render_to_string()), 3);
558 }
559
560 #[test]
561 fn grid_render_does_not_add_visible_space_after_wide_char() {
562 let mut grid = Grid::new(2, 1);
563 let style = CellStyle::default();
564
565 grid.write_str(0, 0, "界", &style);
566
567 assert_eq!(crate::style::visible_len(&grid.render_to_string()), 2);
568 }
569
570 #[test]
571 fn grid_write_str_clears_stale_wide_continuation_after_narrow_overwrite() {
572 let mut grid = Grid::new(2, 1);
573 let style = CellStyle::default();
574
575 grid.write_str(0, 0, "界", &style);
576 grid.write_str(0, 0, "A", &style);
577
578 assert_eq!(grid.render_to_string(), "A ");
579 }
580
581 #[test]
582 fn grid_write_str_clears_wide_char_when_overwriting_its_continuation() {
583 let mut grid = Grid::new(2, 1);
584 let style = CellStyle::default();
585
586 grid.write_str(0, 0, "界", &style);
587 grid.write_str(1, 0, "B", &style);
588
589 assert_eq!(grid.render_to_string(), " B");
590 }
591
592 #[test]
593 fn grid_set_clears_stale_wide_continuation_after_narrow_overwrite() {
594 let mut grid = Grid::new(2, 1);
595 let style = CellStyle::default();
596
597 grid.write_str(0, 0, "界", &style);
598 grid.set(0, 0, Cell::with_char('A'));
599
600 assert_eq!(grid.render_to_string(), "A ");
601 }
602
603 #[test]
604 fn grid_set_clears_wide_char_when_overwriting_its_continuation() {
605 let mut grid = Grid::new(2, 1);
606 let style = CellStyle::default();
607
608 grid.write_str(0, 0, "界", &style);
609 grid.set(1, 0, Cell::with_char('B'));
610
611 assert_eq!(grid.render_to_string(), " B");
612 }
613
614 #[test]
615 fn grid_set_marks_wide_char_continuation() {
616 let mut grid = Grid::new(2, 1);
617
618 grid.set(0, 0, Cell::with_char('界'));
619
620 assert_eq!(grid.get(0, 0).ch, '界');
621 assert_eq!(grid.get(1, 0).ch, WIDE_CONTINUATION);
622 assert_eq!(crate::style::visible_len(&grid.render_to_string()), 2);
623 }
624
625 #[test]
626 fn grid_set_drops_wide_char_that_would_overflow() {
627 let mut grid = Grid::new(2, 1);
628
629 grid.set(1, 0, Cell::with_char('界'));
630
631 assert_eq!(grid.render_to_string(), " ");
632 }
633
634 #[test]
635 fn grid_fill_bg() {
636 let mut grid = Grid::new(10, 5);
637 grid.fill_bg(1, 1, 3, 2, Color::Red);
638 assert_eq!(grid.get(1, 1).bg, Some(Color::Red));
639 assert_eq!(grid.get(3, 2).bg, Some(Color::Red));
640 assert_eq!(grid.get(0, 0).bg, None);
641 }
642
643 #[test]
644 fn grid_fill_bg_styles_wide_span_when_continuation_is_covered() {
645 let mut grid = Grid::new(2, 1);
646 let style = CellStyle::default();
647
648 grid.write_str(0, 0, "界", &style);
649 grid.fill_bg(1, 0, 1, 1, Color::Blue);
650
651 assert_eq!(grid.get(0, 0).bg, Some(Color::Blue));
652 assert_eq!(grid.get(1, 0).bg, Some(Color::Blue));
653 assert!(grid.render_to_string().contains("\x1b["));
654 }
655
656 #[test]
657 fn grid_fill_bg_saturates_overflowing_bounds() {
658 let mut grid = Grid::new(4, 2);
659 grid.fill_bg(u16::MAX - 1, u16::MAX - 1, 10, 10, Color::Red);
660 assert!(grid.cells.iter().flatten().all(|cell| cell.bg.is_none()));
661
662 grid.fill_bg(2, 1, u16::MAX, u16::MAX, Color::Blue);
663 assert_eq!(grid.get(2, 1).bg, Some(Color::Blue));
664 assert_eq!(grid.get(3, 1).bg, Some(Color::Blue));
665 assert_eq!(grid.get(1, 1).bg, None);
666 assert_eq!(grid.get(2, 0).bg, None);
667 }
668
669 #[test]
670 fn grid_diff_detects_changes() {
671 let grid1 = Grid::new(5, 3);
672 let mut grid2 = Grid::new(5, 3);
673 grid2.set(2, 1, Cell::with_char('A'));
674
675 let changes = grid1.diff(&grid2);
676 assert_eq!(changes.len(), 1);
677 assert_eq!(changes[0].x, 2);
678 assert_eq!(changes[0].y, 1);
679 assert_eq!(changes[0].cell.ch, 'A');
680 }
681
682 #[test]
683 fn grid_diff_no_changes() {
684 let grid1 = Grid::new(5, 3);
685 let grid2 = Grid::new(5, 3);
686 let changes = grid1.diff(&grid2);
687 assert!(changes.is_empty());
688 }
689
690 #[test]
691 fn grid_diff_detects_non_empty_cells_in_new_columns() {
692 let grid1 = Grid::new(1, 2);
693 let mut grid2 = Grid::new(3, 2);
694 grid2.set(2, 1, Cell::with_char('B'));
695
696 let changes = grid1.diff(&grid2);
697
698 assert_eq!(changes.len(), 1);
699 assert_eq!(changes[0].x, 2);
700 assert_eq!(changes[0].y, 1);
701 assert_eq!(changes[0].cell.ch, 'B');
702 }
703
704 #[test]
705 fn cell_ansi_plain() {
706 let cell = Cell::with_char('A');
707 assert_eq!(cell.to_ansi(), "A");
708 }
709
710 #[test]
711 fn cell_ansi_styled() {
712 let cell = Cell {
713 ch: 'B',
714 bold: true,
715 reverse: true,
716 fg: Some(Color::Red),
717 ..Default::default()
718 };
719 let ansi = cell.to_ansi();
720 assert!(ansi.contains("\x1b["));
721 assert!(ansi.contains("1"));
722 assert!(ansi.contains("7"));
723 assert!(ansi.contains('B'));
724 }
725}