Skip to main content

smelt_term/
grid.rs

1use super::geometry::Rect;
2pub use smelt_style::style::{Color, Style};
3
4/// Convert `Color` to crossterm's `Color` at the SGR-emit boundary.
5/// A free function because the orphan rule prevents a `From` impl.
6pub(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    /// Mutable cell access; returns `None` when out of bounds.
119    ///
120    /// Escape hatch that **bypasses** the wide-char continuation invariant
121    /// upheld by `set` / `put_char` / `fill`. Callers must own the entire
122    /// region they write into and re-establish the invariant themselves
123    /// (e.g. `snapshot.rs` stamps `\0` continuations explicitly). For any
124    /// normal painting, prefer `set` / `put_str` / `put_char` / `fill`.
125    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    /// Single mutation entry point. Every cell write - `set`, `put_char`,
139    /// `fill`, and their slice equivalents - funnels through here so the
140    /// wide-char continuation invariant is upheld in exactly one place:
141    ///
142    /// 1. A wide char at `(x, y)` implies `(x+1, y).symbol == '\0'`.
143    /// 2. `(x, y).symbol == '\0'` implies `(x-1, y)` holds a wide char.
144    ///
145    /// Three transitions need extra bookkeeping to keep the invariant:
146    ///
147    /// - Overwriting the LEADING half of a wide char (the new cell is
148    ///   narrow): clear the stale `\0` at `x+1` to a plain space so the
149    ///   diff doesn't skip it and the previous frame's right-half doesn't
150    ///   leak through.
151    /// - Overwriting a CONTINUATION cell (was `\0`): break the now-orphaned
152    ///   wide glyph at `x-1` to a plain space; otherwise the diff still
153    ///   thinks the wide is intact.
154    /// - Writing a wide char where `x+1` was itself the leading half of
155    ///   another wide char: clear that displaced wide's continuation at
156    ///   `x+2` so we don't leave a half-wide hanging.
157    ///
158    /// Cleared cells inherit the new cell's style so a styled background
159    /// stays visually contiguous across the now-narrow run.
160    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    /// Overwrites `symbol` and `style.fg`; preserves the existing cell's
216    /// `bg` and text attributes. Use for fg-only painting over a filled background.
217    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    /// String form of [`Grid::put_char`]: overwrites symbol + fg, preserves bg and attrs.
228    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    /// Paint a [`Line`] of styled spans at `(x, y)`, clipping at the right edge.
236    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            // Skip wide-char continuation cells (`\0`); the preceding wide
283            // glyph already covers both visual columns on the terminal.
284            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    /// Absolute rect this slice covers in the underlying grid.
330    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    /// Read a cell at slice-local coords; returns default `Cell` when out of bounds.
365    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    /// Mutable cell access at slice-local coords. See [`Grid::cell_mut`] for caveats.
374    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    /// Slice-local [`Grid::put_char`]: overwrites symbol + fg, preserves bg and attrs.
476    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    /// Slice-local [`Grid::put_line`]: paint spans left-to-right, clipping at the slice edge.
484    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    /// Slice-local [`Grid::put_str_fg`]: overwrites symbol + fg per char, preserves bg and attrs.
503    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    // ── Wide chars ───────────────────────────────────────────────────────
817
818    #[test]
819    fn set_wide_char_marks_next_cell_as_continuation() {
820        // CJK chars have display width 2. The continuation cell carries
821        // '\0' so flush/diff can skip it.
822        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        // Every path that writes a wide char must mark the next cell so
840        // downstream diff/flush can skip it. Otherwise a diff against a
841        // prev frame with non-empty content at the continuation slot
842        // emits a spurious update that overwrites the wide char's right
843        // half on the terminal.
844        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        // Regression for the wide-char bug: if prev had a real char at
866        // the continuation column and curr paints a wide char that covers
867        // it, diff must not yield an update for the continuation column -
868        // otherwise flush_diff overwrites the right half of the wide char.
869        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        // Within a single frame, an earlier paint can leave a wide char at
897        // col N (marking col N+1 as '\0'), and a later paint can overwrite
898        // col N with a narrow char. The narrow overwrite must clear the
899        // stale continuation marker at col N+1 - otherwise the next diff
900        // skips that cell (because it filters '\0'), and the terminal keeps
901        // showing whatever was at col N+1 in the previous frame.
902        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        // Symmetric case: a previous paint wrote a wide char at col N (so
916        // col N+1 is '\0'). A later paint writes a narrow char at col N+1.
917        // The wide char at col N is now orphaned - its visual right half
918        // is occupied by the new narrow char. The leading slot must lose
919        // its wide glyph so flush/diff don't try to render a 2-cell-wide
920        // char that's been broken.
921        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        // End-to-end shape of the leftover-cell bug.
936        //
937        // Frame N (prev): writes a wide char at col 0. Terminal ends up
938        // with `漢` covering cols 0+1.
939        //
940        // Frame N+1 (curr): some paint writes a wide char at col 0 (e.g.
941        // a span or virt text paint), then a later paint overwrites col 0
942        // with a narrow char (e.g. a cursor or overlay). The diff must
943        // still emit an update for col 1 so the terminal clears the right
944        // half of the previous wide char - otherwise that half lingers.
945        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()); // first paint: wide
950        curr.set(0, 0, 'A', Style::default()); // second paint: narrow overwrite
951
952        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    /// Assert the two grid-wide wide-char invariants:
961    ///   I1. `(x, y).symbol == '\0'` only at the cell immediately right of a wide.
962    ///   I2. A wide char at `(x, y)` with `x+1 < width` implies `(x+1, y).symbol == '\0'`.
963    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        // Wide at col 3 (continuation at col 4). Fill clobbers cols 0..4
1011        // inclusive of the leading wide but not its continuation.
1012        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        // Wide at col 0 (continuation at col 1). Fill clobbers cols 1..3
1023        // - leaves the leading slot at col 0 untouched. The leading wide
1024        // must be broken because its right half is gone.
1025        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        // Two adjacent wide chars: `漢` at col 0 (cont at 1), `字` at col 2 (cont at 3).
1036        // Writing a new wide at col 1 displaces the first, lands on the second's leading.
1037        // After the write, the new wide owns cols 1+2; col 0 should be broken (its
1038        // continuation got reassigned) and col 3 should no longer be a stale '\0'.
1039        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        // Col 0 lost its right half, so the wide glyph there must be broken.
1047        assert_ne!(grid.cell(0, 0).symbol, '漢');
1048        // Col 3 was the displaced wide's continuation - it must be cleared.
1049        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        // The reported "leftover after dialog close" scenario, end-to-end:
1070        // prev frame painted a wide char; curr frame overwrites it with a
1071        // narrow before flushing. The continuation cell must end up in the
1072        // diff stream so the terminal clears the right half.
1073        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        // 4-wide grid, write "ab漢": the wide char would land at col 2 and
1112        // would need col 3 too - fits. Try "abc漢" in width 4: 'c' at 2,
1113        // wide char needs 3+4 → overflows, breaks before writing.
1114        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        // Position 3 was never written.
1120        assert_eq!(grid.cell(3, 0).symbol, ' ');
1121    }
1122
1123    // ── Diff over styles ─────────────────────────────────────────────────
1124
1125    #[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    // ── Bounds ───────────────────────────────────────────────────────────
1137
1138    #[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        // No panic; nothing changed.
1143        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        // First row unchanged.
1152        assert_eq!(grid.cell(0, 0).symbol, ' ');
1153    }
1154}