Skip to main content

termgrid_core/
grid.rs

1use crate::{GlyphRegistry, Style};
2use unicode_segmentation::UnicodeSegmentation;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum Cell {
6    Empty,
7    Glyph {
8        grapheme: String,
9        style: Style,
10    },
11    /// Placeholder cell occupied by the trailing half of a width=2 glyph.
12    Continuation,
13}
14
15impl Cell {
16    pub fn is_continuation(&self) -> bool {
17        matches!(self, Cell::Continuation)
18    }
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Grid {
23    pub width: u16,
24    pub height: u16,
25    cells: Vec<Cell>,
26}
27
28impl Grid {
29    pub fn new(width: u16, height: u16) -> Self {
30        let len = width as usize * height as usize;
31        Self {
32            width,
33            height,
34            cells: vec![Cell::Empty; len],
35        }
36    }
37
38    pub fn clear(&mut self) {
39        for c in &mut self.cells {
40            *c = Cell::Empty;
41        }
42    }
43
44    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
45        let idx = self.idx(x, y)?;
46        self.cells.get(idx)
47    }
48
49    pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
50        if let Some(idx) = self.idx(x, y) {
51            self.cells[idx] = cell;
52        }
53    }
54
55    pub fn idx(&self, x: u16, y: u16) -> Option<usize> {
56        if x >= self.width || y >= self.height {
57            return None;
58        }
59        Some(y as usize * self.width as usize + x as usize)
60    }
61
62    fn clear_overlaps_at(&mut self, x: u16, y: u16, reg: &GlyphRegistry) {
63        let here = self.get(x, y).cloned();
64
65        // If we're overwriting the trailing half of a wide glyph, clear the leading half too.
66        if matches!(here, Some(Cell::Continuation)) {
67            self.set(x, y, Cell::Empty);
68
69            if x > 0 {
70                if let Some(Cell::Glyph { grapheme, .. }) = self.get(x - 1, y).cloned() {
71                    if reg.width(&grapheme) == 2 {
72                        self.set(x - 1, y, Cell::Empty);
73                    }
74                }
75            }
76            return;
77        }
78
79        // If we're overwriting the leading half of a wide glyph, clear its continuation.
80        if let Some(Cell::Glyph { grapheme, .. }) = here {
81            if reg.width(&grapheme) == 2
82                && x + 1 < self.width
83                && matches!(self.get(x + 1, y), Some(Cell::Continuation))
84            {
85                self.set(x + 1, y, Cell::Empty);
86            }
87        }
88
89        // If the cell to our left is a wide glyph but we are not its continuation, clear it.
90        if x > 0 {
91            if let Some(Cell::Glyph { grapheme, .. }) = self.get(x - 1, y).cloned() {
92                if reg.width(&grapheme) == 2 && !matches!(self.get(x, y), Some(Cell::Continuation))
93                {
94                    self.set(x - 1, y, Cell::Empty);
95                }
96            }
97        }
98    }
99
100    /// Places `text` starting at (`x`, `y`) using the registry for width policy.
101    ///
102    /// This function is newline-agnostic: `\n` will stop placement.
103    /// Returns the next x position after the last placed glyph.
104    pub fn put_text(
105        &mut self,
106        mut x: u16,
107        y: u16,
108        text: &str,
109        style: Style,
110        reg: &GlyphRegistry,
111    ) -> u16 {
112        if y >= self.height {
113            return x;
114        }
115
116        for g in UnicodeSegmentation::graphemes(text, true) {
117            if g == "\n" || g == "\r" {
118                break;
119            }
120
121            let w = reg.width(g);
122            if x >= self.width {
123                break;
124            }
125            // Avoid placing half of a wide glyph at the right edge.
126            if w == 2 && x + 1 >= self.width {
127                break;
128            }
129
130            // Maintain wide-glyph invariants when overwriting.
131            self.clear_overlaps_at(x, y, reg);
132            if w == 2 {
133                self.clear_overlaps_at(x + 1, y, reg);
134            }
135
136            self.set(
137                x,
138                y,
139                Cell::Glyph {
140                    grapheme: g.to_string(),
141                    style,
142                },
143            );
144
145            if w == 2 {
146                self.set(x + 1, y, Cell::Continuation);
147                x = x.saturating_add(2);
148            } else {
149                x = x.saturating_add(1);
150            }
151        }
152
153        x
154    }
155
156    pub fn rows(&self) -> impl Iterator<Item = &[Cell]> {
157        self.cells.chunks(self.width as usize)
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum InvariantError {
163    ContinuationAtColumn0 { y: u16 },
164    OrphanContinuation { x: u16, y: u16 },
165    MissingContinuationHalf { x: u16, y: u16 },
166}
167
168impl Grid {
169    /// Validate internal grid invariants.
170    ///
171    /// This is intended for tests and debug builds. Production callers can
172    /// enable it via the `debug-validate` feature.
173    pub fn validate_invariants(&self, reg: &GlyphRegistry) -> Result<(), InvariantError> {
174        for y in 0..self.height {
175            for x in 0..self.width {
176                let c = self.get(x, y).expect("in-bounds");
177                match c {
178                    Cell::Continuation => {
179                        if x == 0 {
180                            return Err(InvariantError::ContinuationAtColumn0 { y });
181                        }
182                        let prev = self.get(x - 1, y).expect("in-bounds");
183                        match prev {
184                            Cell::Glyph { grapheme, .. } => {
185                                if reg.width(grapheme) != 2 {
186                                    return Err(InvariantError::OrphanContinuation { x, y });
187                                }
188                            }
189                            _ => return Err(InvariantError::OrphanContinuation { x, y }),
190                        }
191                    }
192                    Cell::Glyph { grapheme, .. } => {
193                        if reg.width(grapheme) == 2 {
194                            if x + 1 >= self.width {
195                                // A width=2 glyph must never be placed half-visible.
196                                return Err(InvariantError::MissingContinuationHalf { x, y });
197                            }
198                            let next = self.get(x + 1, y).expect("in-bounds");
199                            if !matches!(next, Cell::Continuation) {
200                                return Err(InvariantError::MissingContinuationHalf { x, y });
201                            }
202                        }
203                    }
204                    Cell::Empty => {}
205                }
206            }
207        }
208        Ok(())
209    }
210}