1use super::geometry::Rect;
2pub use smelt_style::style::{Color, Style};
3
4pub(crate) fn to_crossterm_color(c: Color) -> crossterm::style::Color {
7 use crossterm::style::Color as X;
8 match c {
9 Color::Reset => X::Reset,
10 Color::Black => X::Black,
11 Color::DarkGrey => X::DarkGrey,
12 Color::Red => X::Red,
13 Color::DarkRed => X::DarkRed,
14 Color::Green => X::Green,
15 Color::DarkGreen => X::DarkGreen,
16 Color::Yellow => X::Yellow,
17 Color::DarkYellow => X::DarkYellow,
18 Color::Blue => X::Blue,
19 Color::DarkBlue => X::DarkBlue,
20 Color::Magenta => X::Magenta,
21 Color::DarkMagenta => X::DarkMagenta,
22 Color::Cyan => X::Cyan,
23 Color::DarkCyan => X::DarkCyan,
24 Color::White => X::White,
25 Color::Grey => X::Grey,
26 Color::Rgb { r, g, b } => X::Rgb { r, g, b },
27 Color::AnsiValue(v) => X::AnsiValue(v),
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum TextAlign {
33 Left,
34 Center,
35 Right,
36}
37
38pub fn display_width(text: &str) -> u16 {
39 use unicode_width::UnicodeWidthStr;
40 UnicodeWidthStr::width(text).min(u16::MAX as usize) as u16
41}
42
43pub fn truncate_width(text: &str, max_width: u16) -> String {
44 let mut out = String::new();
45 write_text(0, max_width, text, |_, ch| out.push(ch));
46 out
47}
48
49pub(crate) fn char_width(ch: char) -> u16 {
50 use unicode_width::UnicodeWidthChar;
51 UnicodeWidthChar::width(ch).unwrap_or(1).max(1) as u16
52}
53
54fn write_text(mut col: u16, limit: u16, text: &str, mut write: impl FnMut(u16, char)) -> u16 {
55 for ch in text.chars() {
56 let width = char_width(ch);
57 if col.saturating_add(width) > limit {
58 break;
59 }
60 write(col, ch);
61 col = col.saturating_add(width);
62 }
63 col.min(limit)
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct Cell {
68 pub symbol: char,
69 pub style: Style,
70}
71
72impl Default for Cell {
73 fn default() -> Self {
74 Self {
75 symbol: ' ',
76 style: Style::default(),
77 }
78 }
79}
80
81#[derive(Clone)]
82pub struct Grid {
83 cells: Vec<Cell>,
84 width: u16,
85 height: u16,
86}
87
88impl Grid {
89 pub fn new(width: u16, height: u16) -> Self {
90 let len = width as usize * height as usize;
91 Self {
92 cells: vec![Cell::default(); len],
93 width,
94 height,
95 }
96 }
97
98 pub fn width(&self) -> u16 {
99 self.width
100 }
101
102 pub fn height(&self) -> u16 {
103 self.height
104 }
105
106 pub fn resize(&mut self, width: u16, height: u16) {
107 self.width = width;
108 self.height = height;
109 self.cells
110 .resize(width as usize * height as usize, Cell::default());
111 self.clear_all();
112 }
113
114 pub fn cell(&self, x: u16, y: u16) -> &Cell {
115 &self.cells[self.idx(x, y)]
116 }
117
118 pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
126 if x < self.width && y < self.height {
127 let idx = self.idx(x, y);
128 Some(&mut self.cells[idx])
129 } else {
130 None
131 }
132 }
133
134 pub fn set(&mut self, x: u16, y: u16, symbol: char, style: Style) {
135 self.write_cell(x, y, Cell { symbol, style });
136 }
137
138 fn write_cell(&mut self, x: u16, y: u16, new_cell: Cell) {
161 use unicode_width::UnicodeWidthChar;
162 if x >= self.width || y >= self.height {
163 return;
164 }
165 let idx = self.idx(x, y);
166 let old_symbol = self.cells[idx].symbol;
167
168 if x + 1 < self.width && UnicodeWidthChar::width(old_symbol).unwrap_or(1) == 2 {
169 let cont = self.idx(x + 1, y);
170 if self.cells[cont].symbol == '\0' {
171 self.cells[cont] = Cell {
172 symbol: ' ',
173 style: new_cell.style,
174 };
175 }
176 }
177 if old_symbol == '\0' && x > 0 {
178 let lead = self.idx(x - 1, y);
179 if UnicodeWidthChar::width(self.cells[lead].symbol).unwrap_or(1) == 2 {
180 self.cells[lead] = Cell {
181 symbol: ' ',
182 style: new_cell.style,
183 };
184 }
185 }
186
187 self.cells[idx] = new_cell;
188
189 if UnicodeWidthChar::width(new_cell.symbol).unwrap_or(1) == 2 && x + 1 < self.width {
190 let cont = self.idx(x + 1, y);
191 let displaced = self.cells[cont].symbol;
192 if UnicodeWidthChar::width(displaced).unwrap_or(1) == 2 && x + 2 < self.width {
193 let dispcont = self.idx(x + 2, y);
194 if self.cells[dispcont].symbol == '\0' {
195 self.cells[dispcont] = Cell {
196 symbol: ' ',
197 style: new_cell.style,
198 };
199 }
200 }
201 self.cells[cont] = Cell {
202 symbol: '\0',
203 style: new_cell.style,
204 };
205 }
206 }
207
208 pub fn put_str(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
209 if y >= self.height {
210 return x.min(self.width);
211 }
212 write_text(x, self.width, text, |col, ch| self.set(col, y, ch, style))
213 }
214
215 pub fn put_char(&mut self, x: u16, y: u16, symbol: char, fg: Color) {
218 if x >= self.width || y >= self.height {
219 return;
220 }
221 let idx = self.idx(x, y);
222 let mut style = self.cells[idx].style;
223 style.fg = Some(fg);
224 self.write_cell(x, y, Cell { symbol, style });
225 }
226
227 pub fn put_str_fg(&mut self, x: u16, y: u16, text: &str, fg: Color) -> u16 {
229 if y >= self.height {
230 return x.min(self.width);
231 }
232 write_text(x, self.width, text, |col, ch| self.put_char(col, y, ch, fg))
233 }
234
235 pub fn put_line(&mut self, x: u16, y: u16, line: &crate::line::Line<'_>) -> u16 {
237 let mut col = x;
238 for span in &line.spans {
239 if col >= self.width {
240 break;
241 }
242 let before = col;
243 col = self.put_str(col, y, span.text.as_ref(), span.style);
244 if col == before {
245 break;
246 }
247 }
248 col.min(self.width)
249 }
250
251 pub fn fill(&mut self, area: Rect, symbol: char, style: Style) {
252 let new_cell = Cell { symbol, style };
253 for row in area.top..area.bottom().min(self.height) {
254 for col in area.left..area.right().min(self.width) {
255 self.write_cell(col, row, new_cell);
256 }
257 }
258 }
259
260 pub fn clear(&mut self, area: Rect) {
261 self.fill(area, ' ', Style::default());
262 }
263
264 pub fn clear_all(&mut self) {
265 for cell in &mut self.cells {
266 *cell = Cell::default();
267 }
268 }
269
270 pub fn slice_mut(&mut self, area: Rect) -> GridSlice<'_> {
271 let area = Rect::new(
272 area.top.min(self.height),
273 area.left.min(self.width),
274 area.width.min(self.width.saturating_sub(area.left)),
275 area.height.min(self.height.saturating_sub(area.top)),
276 );
277 GridSlice { grid: self, area }
278 }
279
280 pub fn diff<'a>(&'a self, prev: &'a Grid) -> impl Iterator<Item = CellUpdate<'a>> {
281 self.cells.iter().enumerate().filter_map(move |(i, cell)| {
282 if cell.symbol == '\0' {
285 return None;
286 }
287 let prev_cell = prev.cells.get(i)?;
288 if cell != prev_cell {
289 let x = (i % self.width as usize) as u16;
290 let y = (i / self.width as usize) as u16;
291 Some(CellUpdate { x, y, cell })
292 } else {
293 None
294 }
295 })
296 }
297
298 pub fn swap_with(&mut self, other: &mut Grid) {
299 std::mem::swap(&mut self.cells, &mut other.cells);
300 std::mem::swap(&mut self.width, &mut other.width);
301 std::mem::swap(&mut self.height, &mut other.height);
302 }
303
304 fn idx(&self, x: u16, y: u16) -> usize {
305 y as usize * self.width as usize + x as usize
306 }
307}
308
309pub struct CellUpdate<'a> {
310 pub x: u16,
311 pub y: u16,
312 pub cell: &'a Cell,
313}
314
315pub struct GridSlice<'a> {
316 grid: &'a mut Grid,
317 area: Rect,
318}
319
320impl<'a> GridSlice<'a> {
321 pub fn width(&self) -> u16 {
322 self.area.width
323 }
324
325 pub fn height(&self) -> u16 {
326 self.area.height
327 }
328
329 pub fn grid_rect(&self) -> Rect {
331 self.area
332 }
333
334 pub fn to_grid_rect(&self, rect: Rect) -> Rect {
335 rect.to_grid(self.area)
336 }
337
338 pub fn to_local_rect(&self, rect: Rect) -> Rect {
339 rect.to_local(self.area)
340 }
341
342 pub fn slice_mut(&mut self, area: Rect) -> GridSlice<'_> {
343 let parent = Rect::new(0, 0, self.area.width, self.area.height);
344 let clipped = area.clip_to(parent);
345 let area = Rect::new(
346 self.area.top.saturating_add(clipped.top),
347 self.area.left.saturating_add(clipped.left),
348 clipped.width,
349 clipped.height,
350 );
351 GridSlice {
352 grid: self.grid,
353 area,
354 }
355 }
356
357 pub fn set(&mut self, x: u16, y: u16, symbol: char, style: Style) {
358 if x < self.area.width && y < self.area.height {
359 self.grid
360 .set(self.area.left + x, self.area.top + y, symbol, style);
361 }
362 }
363
364 pub fn cell(&self, x: u16, y: u16) -> Cell {
366 if x < self.area.width && y < self.area.height {
367 *self.grid.cell(self.area.left + x, self.area.top + y)
368 } else {
369 Cell::default()
370 }
371 }
372
373 pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
375 if x < self.area.width && y < self.area.height {
376 self.grid.cell_mut(self.area.left + x, self.area.top + y)
377 } else {
378 None
379 }
380 }
381
382 pub fn put_str(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
383 self.write_str_at(x, y, text, None, style)
384 }
385
386 fn write_str_at(
387 &mut self,
388 x: u16,
389 y: u16,
390 text: &str,
391 max_width: Option<u16>,
392 style: Style,
393 ) -> u16 {
394 if y >= self.area.height {
395 return x.min(self.area.width);
396 }
397 let abs_y = self.area.top + y;
398 let limit = x
399 .saturating_add(max_width.unwrap_or(u16::MAX))
400 .min(self.area.width);
401 write_text(x, limit, text, |col, ch| {
402 self.grid.set(self.area.left + col, abs_y, ch, style)
403 })
404 }
405
406 pub fn put_padded(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {
407 if y >= self.area.height {
408 return x.min(self.area.width);
409 }
410 let end = x.saturating_add(width).min(self.area.width);
411 let mut col = self
412 .write_str_at(x, y, text, Some(end.saturating_sub(x)), style)
413 .min(end);
414 while col < end {
415 self.set(col, y, ' ', style);
416 col = col.saturating_add(1);
417 }
418 end
419 }
420
421 pub fn put_str_aligned(
422 &mut self,
423 x: u16,
424 y: u16,
425 width: u16,
426 text: &str,
427 align: TextAlign,
428 style: Style,
429 ) -> u16 {
430 if y >= self.area.height {
431 return x.min(self.area.width);
432 }
433 let width = width.min(self.area.width.saturating_sub(x));
434 let text_width = display_width(text).min(width);
435 let col = match align {
436 TextAlign::Left => x,
437 TextAlign::Center => x.saturating_add(width.saturating_sub(text_width) / 2),
438 TextAlign::Right => x.saturating_add(width.saturating_sub(text_width)),
439 };
440 self.put_str(col, y, text, style)
441 }
442
443 pub fn fill_row(&mut self, y: u16, style: Style) {
444 self.fill(Rect::new(y, 0, self.area.width, 1), ' ', style);
445 }
446
447 pub fn rule_h(&mut self, y: u16, style: Style) {
448 self.rule_h_range(0, y, self.area.width, style);
449 }
450
451 pub fn rule_v(&mut self, x: u16, style: Style) {
452 self.rule_v_range(x, 0, self.area.height, style);
453 }
454
455 pub fn rule_h_range(&mut self, x: u16, y: u16, width: u16, style: Style) {
456 if y >= self.area.height || x >= self.area.width {
457 return;
458 }
459 let end = x.saturating_add(width).min(self.area.width);
460 for col in x..end {
461 self.set(col, y, '─', style);
462 }
463 }
464
465 pub fn rule_v_range(&mut self, x: u16, y: u16, height: u16, style: Style) {
466 if x >= self.area.width || y >= self.area.height {
467 return;
468 }
469 let end = y.saturating_add(height).min(self.area.height);
470 for row in y..end {
471 self.set(x, row, '│', style);
472 }
473 }
474
475 pub fn put_char(&mut self, x: u16, y: u16, symbol: char, fg: Color) {
477 if x < self.area.width && y < self.area.height {
478 self.grid
479 .put_char(self.area.left + x, self.area.top + y, symbol, fg);
480 }
481 }
482
483 pub fn put_line(&mut self, x: u16, y: u16, line: &crate::line::Line<'_>) -> u16 {
485 if y >= self.area.height {
486 return x.min(self.area.width);
487 }
488 let mut col = x;
489 for span in &line.spans {
490 if col >= self.area.width {
491 break;
492 }
493 let before = col;
494 col = self.put_str(col, y, span.text.as_ref(), span.style);
495 if col == before {
496 break;
497 }
498 }
499 col.min(self.area.width)
500 }
501
502 pub fn put_str_fg(&mut self, x: u16, y: u16, text: &str, fg: Color) -> u16 {
504 if y >= self.area.height {
505 return x.min(self.area.width);
506 }
507 let abs_y = self.area.top + y;
508 write_text(x, self.area.width, text, |col, ch| {
509 self.grid.put_char(self.area.left + col, abs_y, ch, fg)
510 })
511 }
512
513 pub fn fill(&mut self, area: Rect, symbol: char, style: Style) {
514 let abs = Rect::new(
515 self.area.top + area.top,
516 self.area.left + area.left,
517 area.width.min(self.area.width.saturating_sub(area.left)),
518 area.height.min(self.area.height.saturating_sub(area.top)),
519 );
520 self.grid.fill(abs, symbol, style);
521 }
522
523 pub fn clear(&mut self) {
524 self.grid.clear(self.area);
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn new_grid_filled_with_spaces() {
534 let grid = Grid::new(10, 5);
535 assert_eq!(grid.width(), 10);
536 assert_eq!(grid.height(), 5);
537 assert_eq!(grid.cell(0, 0).symbol, ' ');
538 assert_eq!(grid.cell(9, 4).symbol, ' ');
539 }
540
541 #[test]
542 fn set_and_read_cell() {
543 let mut grid = Grid::new(10, 5);
544 let style = Style::new().fg(Color::Red);
545 grid.set(3, 2, 'X', style);
546 assert_eq!(grid.cell(3, 2).symbol, 'X');
547 assert_eq!(grid.cell(3, 2).style.fg, Some(Color::Red));
548 }
549
550 #[test]
551 fn put_str_writes_chars() {
552 let mut grid = Grid::new(10, 5);
553 grid.put_str(2, 1, "hello", Style::default());
554 assert_eq!(grid.cell(2, 1).symbol, 'h');
555 assert_eq!(grid.cell(3, 1).symbol, 'e');
556 assert_eq!(grid.cell(6, 1).symbol, 'o');
557 assert_eq!(grid.cell(7, 1).symbol, ' ');
558 }
559
560 #[test]
561 fn put_str_clips_at_width() {
562 let mut grid = Grid::new(5, 1);
563 grid.put_str(3, 0, "hello", Style::default());
564 assert_eq!(grid.cell(3, 0).symbol, 'h');
565 assert_eq!(grid.cell(4, 0).symbol, 'e');
566 }
567
568 #[test]
569 fn fill_region() {
570 let mut grid = Grid::new(10, 5);
571 let style = Style::new().bg(Color::Blue);
572 grid.fill(Rect::new(1, 2, 3, 2), '#', style);
573 assert_eq!(grid.cell(2, 1).symbol, '#');
574 assert_eq!(grid.cell(4, 2).symbol, '#');
575 assert_eq!(grid.cell(5, 1).symbol, ' ');
576 }
577
578 #[test]
579 fn diff_yields_changed_cells() {
580 let prev = Grid::new(5, 3);
581 let mut curr = Grid::new(5, 3);
582 curr.set(1, 0, 'A', Style::default());
583 curr.set(3, 2, 'B', Style::default());
584
585 let updates: Vec<_> = curr.diff(&prev).collect();
586 assert_eq!(updates.len(), 2);
587 assert_eq!((updates[0].x, updates[0].y), (1, 0));
588 assert_eq!((updates[1].x, updates[1].y), (3, 2));
589 }
590
591 #[test]
592 fn diff_empty_for_identical_grids() {
593 let a = Grid::new(5, 3);
594 let b = Grid::new(5, 3);
595 assert_eq!(a.diff(&b).count(), 0);
596 }
597
598 #[test]
599 fn slice_writes_offset_correctly() {
600 let mut grid = Grid::new(20, 10);
601 let area = Rect::new(2, 5, 10, 4);
602 {
603 let mut slice = grid.slice_mut(area);
604 assert_eq!(slice.width(), 10);
605 assert_eq!(slice.height(), 4);
606 slice.set(0, 0, 'A', Style::default());
607 slice.put_str(1, 1, "hi", Style::default());
608 }
609 assert_eq!(grid.cell(5, 2).symbol, 'A');
610 assert_eq!(grid.cell(6, 3).symbol, 'h');
611 assert_eq!(grid.cell(7, 3).symbol, 'i');
612 }
613
614 #[test]
615 fn slice_clips_to_bounds() {
616 let mut grid = Grid::new(10, 5);
617 let mut slice = grid.slice_mut(Rect::new(0, 0, 3, 2));
618 slice.put_str(0, 0, "hello world", Style::default());
619 assert_eq!(grid.cell(2, 0).symbol, 'l');
620 assert_eq!(grid.cell(3, 0).symbol, ' ');
621 }
622
623 #[test]
624 fn slice_put_str_returns_clipped_end_column() {
625 let mut grid = Grid::new(6, 1);
626 let mut slice = grid.slice_mut(Rect::new(0, 0, 6, 1));
627 let end = slice.put_str(4, 0, "abc", Style::default());
628 assert_eq!(end, 6);
629 assert_eq!(grid.cell(4, 0).symbol, 'a');
630 assert_eq!(grid.cell(5, 0).symbol, 'b');
631 }
632
633 #[test]
634 fn slice_put_str_counts_wide_chars() {
635 let mut grid = Grid::new(6, 1);
636 let mut slice = grid.slice_mut(Rect::new(0, 0, 6, 1));
637 let end = slice.put_str(1, 0, "a語b", Style::default());
638 assert_eq!(end, 5);
639 assert_eq!(grid.cell(1, 0).symbol, 'a');
640 assert_eq!(grid.cell(2, 0).symbol, '語');
641 assert_eq!(grid.cell(3, 0).symbol, '\0');
642 assert_eq!(grid.cell(4, 0).symbol, 'b');
643 }
644
645 #[test]
646 fn slice_put_padded_fills_remaining_width() {
647 let mut grid = Grid::new(8, 1);
648 let style = Style::new().bg(Color::Blue);
649 let mut slice = grid.slice_mut(Rect::new(0, 0, 8, 1));
650 let end = slice.put_padded(1, 0, 4, "hi", style);
651 assert_eq!(end, 5);
652 assert_eq!(grid.cell(1, 0).symbol, 'h');
653 assert_eq!(grid.cell(2, 0).symbol, 'i');
654 assert_eq!(grid.cell(3, 0).symbol, ' ');
655 assert_eq!(grid.cell(4, 0).style.bg, Some(Color::Blue));
656 }
657
658 #[test]
659 fn slice_put_padded_does_not_split_wide_chars() {
660 let mut grid = Grid::new(4, 1);
661 let mut slice = grid.slice_mut(Rect::new(0, 0, 4, 1));
662 let end = slice.put_padded(0, 0, 2, "a語", Style::default());
663 assert_eq!(end, 2);
664 assert_eq!(grid.cell(0, 0).symbol, 'a');
665 assert_eq!(grid.cell(1, 0).symbol, ' ');
666 assert_eq!(grid.cell(2, 0).symbol, ' ');
667 }
668
669 #[test]
670 fn slice_put_str_aligned_places_text() {
671 let mut grid = Grid::new(12, 3);
672 let mut slice = grid.slice_mut(Rect::new(0, 0, 12, 3));
673 slice.put_str_aligned(2, 0, 8, "ab", TextAlign::Left, Style::default());
674 slice.put_str_aligned(2, 1, 8, "ab", TextAlign::Center, Style::default());
675 slice.put_str_aligned(2, 2, 8, "ab", TextAlign::Right, Style::default());
676 assert_eq!(grid.cell(2, 0).symbol, 'a');
677 assert_eq!(grid.cell(5, 1).symbol, 'a');
678 assert_eq!(grid.cell(8, 2).symbol, 'a');
679 }
680
681 #[test]
682 fn slice_put_str_aligned_ignores_rows_outside_slice() {
683 let mut grid = Grid::new(8, 2);
684 let mut slice = grid.slice_mut(Rect::new(0, 0, 8, 2));
685 let end = slice.put_str_aligned(2, 9, 4, "nope", TextAlign::Left, Style::default());
686 assert_eq!(end, 2);
687 assert_eq!(grid.cell(2, 0).symbol, ' ');
688 }
689
690 #[test]
691 fn slice_rules_clip_safely() {
692 let mut grid = Grid::new(4, 3);
693 let mut slice = grid.slice_mut(Rect::new(0, 0, 4, 3));
694 slice.rule_h_range(2, 1, 99, Style::default());
695 slice.rule_v_range(3, 1, 99, Style::default());
696 slice.rule_h(9, Style::default());
697 slice.rule_v(9, Style::default());
698 assert_eq!(grid.cell(2, 1).symbol, '─');
699 assert_eq!(grid.cell(3, 1).symbol, '│');
700 assert_eq!(grid.cell(3, 2).symbol, '│');
701 }
702
703 #[test]
704 fn slice_mut_is_relative_to_parent() {
705 let mut grid = Grid::new(20, 10);
706 let mut parent = grid.slice_mut(Rect::new(2, 5, 10, 5));
707 {
708 let mut child = parent.slice_mut(Rect::new(1, 2, 3, 2));
709 assert_eq!(child.grid_rect(), Rect::new(3, 7, 3, 2));
710 child.set(0, 0, 'x', Style::default());
711 }
712 assert_eq!(grid.cell(7, 3).symbol, 'x');
713 }
714
715 #[test]
716 fn slice_mut_clips_fully_outside_child_to_empty() {
717 let mut grid = Grid::new(20, 10);
718 let mut parent = grid.slice_mut(Rect::new(2, 5, 10, 5));
719 let child = parent.slice_mut(Rect::new(99, 99, 3, 2));
720 assert_eq!(child.grid_rect(), Rect::new(7, 15, 0, 0));
721 }
722
723 #[test]
724 fn truncate_width_respects_display_width() {
725 assert_eq!(display_width("a語b"), 4);
726 assert_eq!(truncate_width("a語b", 3), "a語");
727 assert_eq!(truncate_width("a語b", 2), "a");
728 }
729
730 #[test]
731 fn resize_clears_grid() {
732 let mut grid = Grid::new(5, 3);
733 grid.set(2, 1, 'A', Style::default());
734 grid.resize(10, 5);
735 assert_eq!(grid.width(), 10);
736 assert_eq!(grid.height(), 5);
737 assert_eq!(grid.cell(2, 1).symbol, ' ');
738 }
739
740 #[test]
741 fn put_char_preserves_bg_and_attrs() {
742 let mut grid = Grid::new(10, 3);
743 let base = Style::new().fg(Color::Yellow).bg(Color::Blue).bold();
744 grid.set(2, 1, '#', base);
745 grid.put_char(2, 1, 'X', Color::Red);
746 let cell = grid.cell(2, 1);
747 assert_eq!(cell.symbol, 'X');
748 assert_eq!(cell.style.fg, Some(Color::Red));
749 assert_eq!(cell.style.bg, Some(Color::Blue));
750 assert!(cell.style.bold);
751 }
752
753 #[test]
754 fn put_char_on_empty_cell_leaves_bg_none() {
755 let mut grid = Grid::new(5, 2);
756 grid.put_char(0, 0, 'A', Color::Green);
757 let cell = grid.cell(0, 0);
758 assert_eq!(cell.symbol, 'A');
759 assert_eq!(cell.style.fg, Some(Color::Green));
760 assert_eq!(cell.style.bg, None);
761 }
762
763 #[test]
764 fn put_line_paints_spans_with_their_styles() {
765 use crate::line::{Line, Span};
766 let mut grid = Grid::new(15, 1);
767 let red = Style::new().fg(Color::Red);
768 let line = Line::from_spans([Span::raw("ab"), Span::styled("CD", red), Span::raw("ef")]);
769 grid.put_line(1, 0, &line);
770 assert_eq!(grid.cell(1, 0).symbol, 'a');
771 assert_eq!(grid.cell(1, 0).style.fg, None);
772 assert_eq!(grid.cell(3, 0).symbol, 'C');
773 assert_eq!(grid.cell(3, 0).style.fg, Some(Color::Red));
774 assert_eq!(grid.cell(5, 0).symbol, 'e');
775 assert_eq!(grid.cell(5, 0).style.fg, None);
776 }
777
778 #[test]
779 fn slice_put_line_clips_at_right_edge() {
780 use crate::line::{Line, Span};
781 let mut grid = Grid::new(10, 1);
782 {
783 let mut slice = grid.slice_mut(Rect::new(0, 2, 5, 1));
784 slice.put_line(0, 0, &Line::from_spans([Span::raw("abcdefgh")]));
785 }
786 assert_eq!(grid.cell(2, 0).symbol, 'a');
787 assert_eq!(grid.cell(6, 0).symbol, 'e');
788 assert_eq!(grid.cell(7, 0).symbol, ' ');
789 }
790
791 #[test]
792 fn slice_put_str_fg_preserves_bg() {
793 let mut grid = Grid::new(10, 2);
794 grid.fill(Rect::new(0, 0, 6, 1), ' ', Style::new().bg(Color::Cyan));
795 {
796 let mut slice = grid.slice_mut(Rect::new(0, 0, 10, 1));
797 slice.put_str_fg(1, 0, "hi", Color::Red);
798 }
799 assert_eq!(grid.cell(1, 0).symbol, 'h');
800 assert_eq!(grid.cell(1, 0).style.fg, Some(Color::Red));
801 assert_eq!(grid.cell(1, 0).style.bg, Some(Color::Cyan));
802 assert_eq!(grid.cell(2, 0).style.bg, Some(Color::Cyan));
803 }
804
805 #[test]
806 fn swap_grids() {
807 let mut a = Grid::new(5, 3);
808 let mut b = Grid::new(5, 3);
809 a.set(0, 0, 'A', Style::default());
810 b.set(0, 0, 'B', Style::default());
811 a.swap_with(&mut b);
812 assert_eq!(a.cell(0, 0).symbol, 'B');
813 assert_eq!(b.cell(0, 0).symbol, 'A');
814 }
815
816 #[test]
819 fn set_wide_char_marks_next_cell_as_continuation() {
820 let mut grid = Grid::new(5, 1);
823 grid.set(1, 0, '漢', Style::default());
824 assert_eq!(grid.cell(1, 0).symbol, '漢');
825 assert_eq!(grid.cell(2, 0).symbol, '\0');
826 }
827
828 #[test]
829 fn put_str_lays_wide_chars_two_columns_apart() {
830 let mut grid = Grid::new(10, 1);
831 grid.put_str(0, 0, "a漢b", Style::default());
832 assert_eq!(grid.cell(0, 0).symbol, 'a');
833 assert_eq!(grid.cell(1, 0).symbol, '漢');
834 assert_eq!(grid.cell(3, 0).symbol, 'b');
835 }
836
837 #[test]
838 fn wide_char_continuation_is_marked_consistently_across_paths() {
839 let via_set = {
845 let mut g = Grid::new(5, 1);
846 g.set(0, 0, '漢', Style::default());
847 g
848 };
849 let via_put_str = {
850 let mut g = Grid::new(5, 1);
851 g.put_str(0, 0, "漢", Style::default());
852 g
853 };
854 let via_put_char = {
855 let mut g = Grid::new(5, 1);
856 g.put_char(0, 0, '漢', Color::Reset);
857 g
858 };
859 assert_eq!(via_set.cell(1, 0).symbol, via_put_str.cell(1, 0).symbol);
860 assert_eq!(via_set.cell(1, 0).symbol, via_put_char.cell(1, 0).symbol);
861 }
862
863 #[test]
864 fn diff_does_not_emit_update_for_cell_under_a_wide_char() {
865 let mut prev = Grid::new(5, 1);
870 prev.set(1, 0, 'X', Style::default());
871 let mut curr = Grid::new(5, 1);
872 curr.put_str(0, 0, "漢", Style::default());
873 let updates: Vec<_> = curr.diff(&prev).collect();
874 let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
875 assert_eq!(
876 cols,
877 vec![0],
878 "expected one update at the wide char's column only; got {cols:?}"
879 );
880 }
881
882 #[test]
883 fn slice_put_str_lays_wide_chars_two_columns_apart() {
884 let mut grid = Grid::new(10, 1);
885 {
886 let mut slice = grid.slice_mut(Rect::new(0, 0, 10, 1));
887 slice.put_str(0, 0, "a漢b", Style::default());
888 }
889 assert_eq!(grid.cell(0, 0).symbol, 'a');
890 assert_eq!(grid.cell(1, 0).symbol, '漢');
891 assert_eq!(grid.cell(3, 0).symbol, 'b');
892 }
893
894 #[test]
895 fn overwriting_wide_char_with_narrow_clears_continuation_marker() {
896 let mut grid = Grid::new(5, 1);
903 grid.set(0, 0, '漢', Style::default());
904 assert_eq!(grid.cell(1, 0).symbol, '\0');
905 grid.set(0, 0, 'A', Style::default());
906 assert_ne!(
907 grid.cell(1, 0).symbol,
908 '\0',
909 "narrow overwrite must clear stale continuation marker"
910 );
911 }
912
913 #[test]
914 fn overwriting_wide_continuation_with_narrow_clears_leading_wide_glyph() {
915 let mut grid = Grid::new(5, 1);
922 grid.set(0, 0, '漢', Style::default());
923 assert_eq!(grid.cell(0, 0).symbol, '漢');
924 assert_eq!(grid.cell(1, 0).symbol, '\0');
925 grid.set(1, 0, 'A', Style::default());
926 assert_ne!(
927 grid.cell(0, 0).symbol,
928 '漢',
929 "writing to a wide char's continuation must break the leading glyph"
930 );
931 }
932
933 #[test]
934 fn diff_clears_terminal_when_wide_is_overwritten_with_narrow_in_same_frame() {
935 let mut prev = Grid::new(5, 1);
946 prev.set(0, 0, '漢', Style::default());
947
948 let mut curr = Grid::new(5, 1);
949 curr.set(0, 0, '漢', Style::default()); curr.set(0, 0, 'A', Style::default()); let updates: Vec<_> = curr.diff(&prev).collect();
953 let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
954 assert!(
955 cols.contains(&1),
956 "expected diff to clear the orphaned continuation at col 1; got {cols:?}"
957 );
958 }
959
960 fn assert_grid_invariants(grid: &Grid) {
964 use unicode_width::UnicodeWidthChar;
965 for y in 0..grid.height() {
966 for x in 0..grid.width() {
967 let cell = grid.cell(x, y);
968 if cell.symbol == '\0' {
969 assert!(x > 0, "continuation at column 0 has no leading cell");
970 let lead = grid.cell(x - 1, y).symbol;
971 assert_eq!(
972 UnicodeWidthChar::width(lead).unwrap_or(1),
973 2,
974 "orphaned continuation at ({x}, {y}); leading cell symbol is {lead:?}"
975 );
976 }
977 if UnicodeWidthChar::width(cell.symbol).unwrap_or(1) == 2 && x + 1 < grid.width() {
978 let cont = grid.cell(x + 1, y).symbol;
979 assert_eq!(
980 cont, '\0',
981 "wide char at ({x}, {y}) is missing continuation; got {cont:?}"
982 );
983 }
984 }
985 }
986 }
987
988 #[test]
989 fn put_char_narrow_over_wide_keeps_invariant() {
990 let mut grid = Grid::new(5, 1);
991 grid.set(0, 0, '漢', Style::default());
992 grid.put_char(0, 0, 'A', Color::Red);
993 assert_grid_invariants(&grid);
994 assert_eq!(grid.cell(0, 0).symbol, 'A');
995 assert_ne!(grid.cell(1, 0).symbol, '\0');
996 }
997
998 #[test]
999 fn put_char_on_continuation_breaks_leading_wide() {
1000 let mut grid = Grid::new(5, 1);
1001 grid.set(0, 0, '漢', Style::default());
1002 grid.put_char(1, 0, 'B', Color::Green);
1003 assert_grid_invariants(&grid);
1004 assert_ne!(grid.cell(0, 0).symbol, '漢');
1005 assert_eq!(grid.cell(1, 0).symbol, 'B');
1006 }
1007
1008 #[test]
1009 fn fill_partially_overlapping_a_wide_char_keeps_invariant() {
1010 let mut grid = Grid::new(6, 1);
1013 grid.set(3, 0, '漢', Style::default());
1014 grid.fill(Rect::new(0, 0, 4, 1), '#', Style::default());
1015 assert_grid_invariants(&grid);
1016 assert_eq!(grid.cell(3, 0).symbol, '#');
1017 assert_ne!(grid.cell(4, 0).symbol, '\0');
1018 }
1019
1020 #[test]
1021 fn fill_starting_on_a_continuation_breaks_the_leading_wide() {
1022 let mut grid = Grid::new(5, 1);
1026 grid.set(0, 0, '漢', Style::default());
1027 grid.fill(Rect::new(0, 1, 2, 1), '#', Style::default());
1028 assert_grid_invariants(&grid);
1029 assert_ne!(grid.cell(0, 0).symbol, '漢');
1030 assert_eq!(grid.cell(1, 0).symbol, '#');
1031 }
1032
1033 #[test]
1034 fn writing_wide_over_a_wide_displaces_the_old_continuation() {
1035 let mut grid = Grid::new(6, 1);
1040 grid.set(0, 0, '漢', Style::default());
1041 grid.set(2, 0, '字', Style::default());
1042 grid.set(1, 0, '日', Style::default());
1043 assert_grid_invariants(&grid);
1044 assert_eq!(grid.cell(1, 0).symbol, '日');
1045 assert_eq!(grid.cell(2, 0).symbol, '\0');
1046 assert_ne!(grid.cell(0, 0).symbol, '漢');
1048 assert_ne!(grid.cell(3, 0).symbol, '\0');
1050 }
1051
1052 #[test]
1053 fn clear_all_holds_invariant_after_arbitrary_paint() {
1054 let mut grid = Grid::new(8, 2);
1055 grid.set(0, 0, '漢', Style::default());
1056 grid.set(3, 0, '字', Style::default());
1057 grid.put_str(0, 1, "a漢b", Style::default());
1058 grid.clear_all();
1059 assert_grid_invariants(&grid);
1060 for y in 0..2 {
1061 for x in 0..8 {
1062 assert_eq!(grid.cell(x, y).symbol, ' ');
1063 }
1064 }
1065 }
1066
1067 #[test]
1068 fn diff_emits_continuation_clear_when_wide_was_overwritten() {
1069 let mut prev = Grid::new(6, 1);
1074 prev.set(2, 0, '漢', Style::default());
1075 let mut curr = Grid::new(6, 1);
1076 curr.set(2, 0, '漢', Style::default());
1077 curr.set(2, 0, 'A', Style::default());
1078 let updates: Vec<_> = curr.diff(&prev).collect();
1079 let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
1080 assert!(
1081 cols.contains(&3),
1082 "expected diff to clear orphaned continuation at col 3; got {cols:?}"
1083 );
1084 assert_grid_invariants(&curr);
1085 }
1086
1087 #[test]
1088 fn slice_fill_partial_wide_overlap_keeps_invariant() {
1089 let mut grid = Grid::new(10, 1);
1090 grid.set(4, 0, '漢', Style::default());
1091 {
1092 let mut slice = grid.slice_mut(Rect::new(0, 0, 5, 1));
1093 slice.fill(Rect::new(0, 0, 5, 1), '#', Style::default());
1094 }
1095 assert_grid_invariants(&grid);
1096 }
1097
1098 #[test]
1099 fn put_str_fg_over_wide_keeps_invariant() {
1100 let mut grid = Grid::new(6, 1);
1101 grid.set(1, 0, '漢', Style::default());
1102 grid.put_str_fg(0, 0, "abc", Color::Red);
1103 assert_grid_invariants(&grid);
1104 assert_eq!(grid.cell(0, 0).symbol, 'a');
1105 assert_eq!(grid.cell(1, 0).symbol, 'b');
1106 assert_eq!(grid.cell(2, 0).symbol, 'c');
1107 }
1108
1109 #[test]
1110 fn put_str_breaks_when_wide_char_would_overflow() {
1111 let mut grid = Grid::new(4, 1);
1115 grid.put_str(0, 0, "abc漢", Style::default());
1116 assert_eq!(grid.cell(0, 0).symbol, 'a');
1117 assert_eq!(grid.cell(1, 0).symbol, 'b');
1118 assert_eq!(grid.cell(2, 0).symbol, 'c');
1119 assert_eq!(grid.cell(3, 0).symbol, ' ');
1121 }
1122
1123 #[test]
1126 fn diff_picks_up_style_only_change() {
1127 let mut prev = Grid::new(5, 1);
1128 prev.set(0, 0, 'X', Style::default());
1129 let mut curr = Grid::new(5, 1);
1130 curr.set(0, 0, 'X', Style::new().fg(Color::Red));
1131 let updates: Vec<_> = curr.diff(&prev).collect();
1132 assert_eq!(updates.len(), 1);
1133 assert_eq!(updates[0].cell.style.fg, Some(Color::Red));
1134 }
1135
1136 #[test]
1139 fn set_out_of_bounds_is_silent_noop() {
1140 let mut grid = Grid::new(3, 2);
1141 grid.set(99, 99, 'X', Style::default());
1142 assert_eq!(grid.cell(0, 0).symbol, ' ');
1144 assert_eq!(grid.cell(2, 1).symbol, ' ');
1145 }
1146
1147 #[test]
1148 fn put_str_skips_when_y_out_of_bounds() {
1149 let mut grid = Grid::new(5, 2);
1150 grid.put_str(0, 99, "hello", Style::default());
1151 assert_eq!(grid.cell(0, 0).symbol, ' ');
1153 }
1154}