1use crate::types::Dimensions;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct Position {
11 pub row: usize,
13 pub col: usize,
15}
16
17impl Position {
18 #[must_use]
20 pub const fn new(row: usize, col: usize) -> Self {
21 Self { row, col }
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Region {
28 pub start: Position,
30 pub end: Position,
32}
33
34impl Region {
35 #[must_use]
37 pub const fn new(start: Position, end: Position) -> Self {
38 Self { start, end }
39 }
40
41 #[must_use]
43 pub const fn from_coords(
44 start_row: usize,
45 start_col: usize,
46 end_row: usize,
47 end_col: usize,
48 ) -> Self {
49 Self {
50 start: Position::new(start_row, start_col),
51 end: Position::new(end_row, end_col),
52 }
53 }
54
55 #[must_use]
57 pub const fn width(&self) -> usize {
58 self.end.col.saturating_sub(self.start.col)
59 }
60
61 #[must_use]
63 pub const fn height(&self) -> usize {
64 self.end.row.saturating_sub(self.start.row)
65 }
66
67 #[must_use]
69 pub const fn contains(&self, pos: Position) -> bool {
70 pos.row >= self.start.row
71 && pos.row < self.end.row
72 && pos.col >= self.start.col
73 && pos.col < self.end.col
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79#[allow(clippy::struct_excessive_bools)]
80pub struct CellAttributes {
81 pub bold: bool,
83 pub italic: bool,
85 pub underline: bool,
87 pub blink: bool,
89 pub inverse: bool,
91 pub hidden: bool,
93 pub strikethrough: bool,
95 pub foreground: Option<Color>,
97 pub background: Option<Color>,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum Color {
104 Indexed(u8),
106 Rgb(u8, u8, u8),
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Cell {
113 pub char: char,
115 pub attrs: CellAttributes,
117 pub width: u8,
119}
120
121impl Default for Cell {
122 fn default() -> Self {
123 Self {
124 char: ' ',
125 attrs: CellAttributes::default(),
126 width: 1,
127 }
128 }
129}
130
131pub struct ScreenBuffer {
137 cells: Vec<Vec<Cell>>,
139 dimensions: Dimensions,
141 cursor: Position,
143 saved_cursor: Option<Position>,
145 scroll_region: Option<(usize, usize)>,
147}
148
149impl ScreenBuffer {
150 #[must_use]
152 pub fn new(dimensions: Dimensions) -> Self {
153 let rows = dimensions.rows as usize;
154 let cols = dimensions.cols as usize;
155
156 let cells = (0..rows).map(|_| vec![Cell::default(); cols]).collect();
157
158 Self {
159 cells,
160 dimensions,
161 cursor: Position::default(),
162 saved_cursor: None,
163 scroll_region: None,
164 }
165 }
166
167 #[must_use]
169 pub const fn dimensions(&self) -> Dimensions {
170 self.dimensions
171 }
172
173 #[must_use]
175 pub const fn cursor(&self) -> Position {
176 self.cursor
177 }
178
179 pub fn set_cursor(&mut self, pos: Position) {
181 self.cursor = Position {
182 row: pos.row.min(self.dimensions.rows as usize - 1),
183 col: pos.col.min(self.dimensions.cols as usize - 1),
184 };
185 }
186
187 pub fn move_cursor(&mut self, rows: isize, cols: isize) {
189 let new_row = (self.cursor.row as isize + rows)
190 .max(0)
191 .min(self.dimensions.rows as isize - 1) as usize;
192 let new_col = (self.cursor.col as isize + cols)
193 .max(0)
194 .min(self.dimensions.cols as isize - 1) as usize;
195 self.cursor = Position::new(new_row, new_col);
196 }
197
198 pub const fn save_cursor(&mut self) {
200 self.saved_cursor = Some(self.cursor);
201 }
202
203 pub const fn restore_cursor(&mut self) {
205 if let Some(pos) = self.saved_cursor {
206 self.cursor = pos;
207 }
208 }
209
210 #[must_use]
212 pub fn get(&self, row: usize, col: usize) -> Option<&Cell> {
213 self.cells.get(row).and_then(|r| r.get(col))
214 }
215
216 pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
218 self.cells.get_mut(row).and_then(|r| r.get_mut(col))
219 }
220
221 pub fn put_char(&mut self, c: char, attrs: CellAttributes) {
223 if self.cursor.row < self.cells.len() && self.cursor.col < self.cells[0].len() {
224 self.cells[self.cursor.row][self.cursor.col] = Cell {
225 char: c,
226 attrs,
227 width: if c.is_ascii() { 1 } else { 2 },
228 };
229 self.cursor.col += 1;
230 if self.cursor.col >= self.dimensions.cols as usize {
231 self.cursor.col = 0;
232 self.cursor.row += 1;
233 }
234 }
235 }
236
237 #[must_use]
239 pub fn line(&self, row: usize) -> Option<String> {
240 self.cells.get(row).map(|cells| {
241 cells
242 .iter()
243 .map(|c| c.char)
244 .collect::<String>()
245 .trim_end()
246 .to_string()
247 })
248 }
249
250 #[must_use]
252 pub fn lines(&self) -> Vec<String> {
253 (0..self.dimensions.rows as usize)
254 .filter_map(|row| self.line(row))
255 .collect()
256 }
257
258 #[must_use]
260 pub fn content(&self) -> String {
261 self.lines().join("\n")
262 }
263
264 #[must_use]
266 pub fn region_text(&self, region: Region) -> String {
267 let mut result = String::new();
268 for row in region.start.row..region.end.row.min(self.cells.len()) {
269 if row < self.cells.len() {
270 let start = region.start.col;
271 let end = region.end.col.min(self.cells[row].len());
272 for col in start..end {
273 result.push(self.cells[row][col].char);
274 }
275 if row < region.end.row - 1 {
276 result.push('\n');
277 }
278 }
279 }
280 result.trim_end().to_string()
281 }
282
283 pub fn clear(&mut self) {
285 for row in &mut self.cells {
286 for cell in row {
287 *cell = Cell::default();
288 }
289 }
290 self.cursor = Position::default();
291 }
292
293 pub fn clear_region(&mut self, region: Region) {
295 for row in region.start.row..region.end.row.min(self.cells.len()) {
296 let start = region.start.col;
297 let end = region.end.col.min(self.cells[row].len());
298 for col in start..end {
299 self.cells[row][col] = Cell::default();
300 }
301 }
302 }
303
304 pub fn scroll_up(&mut self, n: usize) {
306 let (start, end) = self
307 .scroll_region
308 .unwrap_or((0, self.dimensions.rows as usize));
309
310 for _ in 0..n {
311 if start < end && end <= self.cells.len() {
312 self.cells.remove(start);
313 self.cells.insert(
314 end - 1,
315 vec![Cell::default(); self.dimensions.cols as usize],
316 );
317 }
318 }
319 }
320
321 pub fn scroll_down(&mut self, n: usize) {
323 let (start, end) = self
324 .scroll_region
325 .unwrap_or((0, self.dimensions.rows as usize));
326
327 for _ in 0..n {
328 if start < end && end <= self.cells.len() {
329 self.cells.remove(end - 1);
330 self.cells
331 .insert(start, vec![Cell::default(); self.dimensions.cols as usize]);
332 }
333 }
334 }
335
336 pub const fn set_scroll_region(&mut self, top: usize, bottom: usize) {
338 if top < bottom && bottom <= self.dimensions.rows as usize {
339 self.scroll_region = Some((top, bottom));
340 } else {
341 self.scroll_region = None;
342 }
343 }
344
345 pub fn resize(&mut self, dimensions: Dimensions) {
347 let new_rows = dimensions.rows as usize;
348 let new_cols = dimensions.cols as usize;
349
350 self.cells
352 .resize_with(new_rows, || vec![Cell::default(); new_cols]);
353
354 for row in &mut self.cells {
356 row.resize_with(new_cols, Cell::default);
357 }
358
359 self.dimensions = dimensions;
360
361 self.cursor.row = self.cursor.row.min(new_rows.saturating_sub(1));
363 self.cursor.col = self.cursor.col.min(new_cols.saturating_sub(1));
364 }
365}
366
367impl std::fmt::Debug for ScreenBuffer {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 f.debug_struct("ScreenBuffer")
370 .field("dimensions", &self.dimensions)
371 .field("cursor", &self.cursor)
372 .finish()
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn screen_buffer_basic() {
382 let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
383
384 screen.put_char('H', CellAttributes::default());
385 screen.put_char('i', CellAttributes::default());
386
387 assert_eq!(screen.line(0), Some("Hi".to_string()));
388 }
389
390 #[test]
391 fn screen_buffer_region() {
392 let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
393
394 for c in "Hello".chars() {
395 screen.put_char(c, CellAttributes::default());
396 }
397
398 let text = screen.region_text(Region::from_coords(0, 0, 1, 5));
399 assert_eq!(text, "Hello");
400 }
401
402 #[test]
403 fn screen_buffer_resize() {
404 let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
405 screen.resize(Dimensions {
406 rows: 40,
407 cols: 120,
408 });
409
410 assert_eq!(screen.dimensions().rows, 40);
411 assert_eq!(screen.dimensions().cols, 120);
412 }
413
414 #[test]
415 fn position_region() {
416 let region = Region::from_coords(0, 0, 10, 20);
417
418 assert!(region.contains(Position::new(5, 10)));
419 assert!(!region.contains(Position::new(10, 10)));
420 assert!(!region.contains(Position::new(5, 20)));
421
422 assert_eq!(region.width(), 20);
423 assert_eq!(region.height(), 10);
424 }
425}