Skip to main content

badterm_core/
screen.rs

1//! The character-cell screen model: a grid of CP437 glyphs with attributes,
2//! a cursor, a scrolling region and a scrollback buffer.
3
4use std::collections::VecDeque;
5
6use crate::palette::{self, Rgb, VGA};
7
8/// A cell color. PC colors keep ANSI-BBS semantics (bold brightens the
9/// foreground, blink brightens the background in iCE mode) by being resolved
10/// at render time rather than when written.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Color {
13    /// VGA palette index 0-15.
14    Pc(u8),
15    /// xterm 256-color index.
16    Xterm(u8),
17    /// 24-bit color.
18    Rgb(Rgb),
19}
20
21pub mod flags {
22    pub const BOLD: u8 = 1 << 0;
23    pub const BLINK: u8 = 1 << 1;
24    pub const UNDERLINE: u8 = 1 << 2;
25    pub const REVERSE: u8 = 1 << 3;
26    pub const CONCEAL: u8 = 1 << 4;
27    pub const FAINT: u8 = 1 << 5;
28}
29
30pub const DEFAULT_FG: Color = Color::Pc(7);
31pub const DEFAULT_BG: Color = Color::Pc(0);
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct Cell {
35    /// CP437 glyph index.
36    pub ch: u8,
37    pub fg: Color,
38    pub bg: Color,
39    pub flags: u8,
40    /// Pixel graphics over the cell (sixel and the like): an ID in the
41    /// screen's [`Tiles`], or 0 for none. Writing to the cell replaces it.
42    pub tile: u32,
43}
44
45impl Default for Cell {
46    fn default() -> Self {
47        Cell { ch: b' ', fg: DEFAULT_FG, bg: DEFAULT_BG, flags: 0, tile: 0 }
48    }
49}
50
51impl Cell {
52    pub fn new(ch: u8, fg: Color, bg: Color) -> Self {
53        Cell { ch, fg, bg, flags: 0, tile: 0 }
54    }
55
56    pub fn is_blank(&self) -> bool {
57        (self.ch == b' ' || self.ch == 0) && self.bg == DEFAULT_BG && self.flags & flags::REVERSE == 0 && self.tile == 0
58    }
59
60    /// Resolve to concrete (foreground, background, blinks) colors.
61    ///
62    /// With `ice` set, the blink attribute selects a high-intensity background
63    /// instead of blinking text, as on a VGA with blink disabled.
64    pub fn resolve(&self, ice: bool) -> (Rgb, Rgb, bool) {
65        self.resolve_with(ice, Look::standard())
66    }
67
68    /// [`Cell::resolve`] with the palette and modes the BBS has set.
69    pub fn resolve_with(&self, ice: bool, look: &Look) -> (Rgb, Rgb, bool) {
70        let bold = self.flags & flags::BOLD != 0 && !look.bright_plain;
71        let blink = self.flags & flags::BLINK != 0;
72        let mut fg = match self.fg {
73            Color::Pc(i) if bold && i < 8 => look.rgb(Color::Pc(i + 8)),
74            c => look.rgb(c),
75        };
76        let mut bg = match self.bg {
77            Color::Pc(i) if blink && ice && i < 8 => look.rgb(Color::Pc(i + 8)),
78            c => look.rgb(c),
79        };
80        if self.flags & flags::REVERSE != 0 {
81            std::mem::swap(&mut fg, &mut bg);
82        }
83        if self.flags & flags::CONCEAL != 0 {
84            fg = bg;
85        }
86        (fg, bg, blink && !ice && !look.blink_steady)
87    }
88}
89
90/// What a BBS can change about how cells are drawn, beyond their own
91/// attributes: CTerm's palette (`OSC 4`) and font slots (`CSI n;m SP D`).
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct Look {
94    /// In xterm order: the 16 ANSI colors (black, red, green, ...), the
95    /// 6x6x6 cube and the grays.
96    pub palette: [Rgb; 256],
97    /// CTerm font IDs for the normal, bright, blink and bright-blink slots.
98    pub fonts: [u8; 4],
99    /// `CSI ?31h`: bright text is drawn in font slot 1.
100    pub bright_font: bool,
101    /// `CSI ?32h`: bold does not brighten the color.
102    pub bright_plain: bool,
103    /// `CSI ?34h`: blinking text is drawn in font slot 2.
104    pub blink_font: bool,
105    /// `CSI ?35h`: blink does not make text blink.
106    pub blink_steady: bool,
107}
108
109impl Default for Look {
110    fn default() -> Look {
111        Look::standard().clone()
112    }
113}
114
115impl Look {
116    /// The VGA palette and font, with no CTerm modes set.
117    pub fn standard() -> &'static Look {
118        static STANDARD: std::sync::LazyLock<Look> = std::sync::LazyLock::new(|| Look {
119            palette: std::array::from_fn(|i| palette::xterm256(i as u8)),
120            fonts: [0; 4],
121            bright_font: false,
122            bright_plain: false,
123            blink_font: false,
124            blink_steady: false,
125        });
126        &STANDARD
127    }
128
129    pub fn rgb(&self, c: Color) -> Rgb {
130        match c {
131            // VGA order to xterm order.
132            Color::Pc(i) => {
133                let i = i & 15;
134                let ansi = palette::ANSI_TO_VGA.iter().position(|&v| v == i & 7).unwrap_or(0) as u8;
135                self.palette[(ansi + (i & 8)) as usize]
136            }
137            Color::Xterm(i) => self.palette[i as usize],
138            Color::Rgb(rgb) => rgb,
139        }
140    }
141
142    /// The CTerm font a cell is drawn in.
143    pub fn font_for(&self, cell: &Cell) -> u8 {
144        let bright = self.bright_font && cell.flags & flags::BOLD != 0;
145        let blink = self.blink_font && cell.flags & flags::BLINK != 0;
146        self.fonts[bright as usize | (blink as usize) << 1]
147    }
148}
149
150pub fn color_rgb(c: Color) -> Rgb {
151    match c {
152        Color::Pc(i) => VGA[(i & 15) as usize],
153        Color::Xterm(i) => palette::xterm256(i),
154        Color::Rgb(rgb) => rgb,
155    }
156}
157
158#[derive(Clone, Copy, Debug)]
159struct SavedCursor {
160    x: usize,
161    y: usize,
162    pen: Cell,
163}
164
165pub struct Screen {
166    cols: usize,
167    rows: usize,
168    cells: Vec<Cell>,
169    x: usize,
170    y: usize,
171    /// Set when a glyph was written to the last column; the wrap happens on the
172    /// next printable character (VT100 "last column flag").
173    pending_wrap: bool,
174    /// Attributes applied to newly written glyphs (`ch` is unused).
175    pub pen: Cell,
176    saved: Option<SavedCursor>,
177    scroll_top: usize,
178    scroll_bottom: usize,
179    pub autowrap: bool,
180    pub cursor_visible: bool,
181    pub ice_colors: bool,
182    /// The palette and fonts the BBS has chosen.
183    pub look: Look,
184    /// The font (CTerm ID) a reset returns to: the entry's choice.
185    pub base_font: u8,
186    /// Whether a reset leaves iCE colors on: the entry's choice.
187    pub base_ice: bool,
188    scrollback: VecDeque<Vec<Cell>>,
189    scrollback_limit: usize,
190    /// The pixel graphics cells show (see [`Cell::tile`]).
191    pub tiles: Tiles,
192}
193
194impl Screen {
195    pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
196        let cols = cols.max(1);
197        let rows = rows.max(1);
198        Screen {
199            cols,
200            rows,
201            cells: vec![Cell::default(); cols * rows],
202            x: 0,
203            y: 0,
204            pending_wrap: false,
205            pen: Cell::default(),
206            saved: None,
207            scroll_top: 0,
208            scroll_bottom: rows - 1,
209            autowrap: true,
210            cursor_visible: true,
211            ice_colors: false,
212            look: Look::default(),
213            base_font: 0,
214            base_ice: false,
215            scrollback: VecDeque::new(),
216            scrollback_limit,
217            tiles: Tiles::default(),
218        }
219    }
220
221    pub fn cols(&self) -> usize {
222        self.cols
223    }
224
225    pub fn rows(&self) -> usize {
226        self.rows
227    }
228
229    pub fn cursor(&self) -> (usize, usize) {
230        (self.x, self.y)
231    }
232
233    pub fn row(&self, y: usize) -> &[Cell] {
234        &self.cells[y * self.cols..(y + 1) * self.cols]
235    }
236
237    pub fn cell_mut(&mut self, x: usize, y: usize) -> Option<&mut Cell> {
238        if x < self.cols && y < self.rows { Some(&mut self.cells[y * self.cols + x]) } else { None }
239    }
240
241    pub fn scrollback_len(&self) -> usize {
242        self.scrollback.len()
243    }
244
245    /// Line `i` of the scrollback, 0 being the oldest.
246    pub fn scrollback_line(&self, i: usize) -> &[Cell] {
247        &self.scrollback[i]
248    }
249
250    pub fn clear_scrollback(&mut self) {
251        self.scrollback.clear();
252    }
253
254    /// Full reset (RIS): clears the screen and modes but keeps scrollback.
255    pub fn reset(&mut self) {
256        self.pen = Cell::default();
257        self.cells.fill(Cell::default());
258        self.x = 0;
259        self.y = 0;
260        self.pending_wrap = false;
261        self.saved = None;
262        self.scroll_top = 0;
263        self.scroll_bottom = self.rows - 1;
264        self.autowrap = true;
265        self.cursor_visible = true;
266        self.ice_colors = self.base_ice;
267        self.look = Look::default();
268        self.look.fonts[0] = self.base_font;
269    }
270
271    pub fn resize(&mut self, cols: usize, rows: usize) {
272        let cols = cols.max(1);
273        let rows = rows.max(1);
274        if cols == self.cols && rows == self.rows {
275            return;
276        }
277        let mut cells = vec![Cell::default(); cols * rows];
278        for y in 0..rows.min(self.rows) {
279            let n = cols.min(self.cols);
280            cells[y * cols..y * cols + n].copy_from_slice(&self.row(y)[..n]);
281        }
282        self.cells = cells;
283        self.cols = cols;
284        self.rows = rows;
285        self.x = self.x.min(cols - 1);
286        self.y = self.y.min(rows - 1);
287        self.scroll_top = 0;
288        self.scroll_bottom = rows - 1;
289        self.pending_wrap = false;
290    }
291
292    /// The cell used when erasing: a space in the current colors (ANSI.SYS
293    /// and ANSI-BBS erase with the current background).
294    fn blank(&self) -> Cell {
295        Cell { ch: b' ', flags: self.pen.flags & (flags::BLINK | flags::REVERSE), ..self.pen }
296    }
297
298    /// Write a glyph at the cursor and advance.
299    pub fn put(&mut self, ch: u8) {
300        if self.pending_wrap {
301            self.pending_wrap = false;
302            self.x = 0;
303            self.line_feed();
304        }
305        let idx = self.y * self.cols + self.x;
306        self.cells[idx] = Cell { ch, ..self.pen };
307        if self.x + 1 < self.cols {
308            self.x += 1;
309        } else if self.autowrap {
310            self.pending_wrap = true;
311        }
312    }
313
314    pub fn carriage_return(&mut self) {
315        self.x = 0;
316        self.pending_wrap = false;
317    }
318
319    /// Move down one line, scrolling the region if at its bottom.
320    pub fn line_feed(&mut self) {
321        self.pending_wrap = false;
322        if self.y == self.scroll_bottom {
323            self.scroll_up(1);
324        } else if self.y + 1 < self.rows {
325            self.y += 1;
326        }
327    }
328
329    /// Move up one line, scrolling the region down if at its top (ESC M).
330    pub fn reverse_index(&mut self) {
331        self.pending_wrap = false;
332        if self.y == self.scroll_top {
333            self.scroll_down(1);
334        } else if self.y > 0 {
335            self.y -= 1;
336        }
337    }
338
339    pub fn backspace(&mut self) {
340        self.pending_wrap = false;
341        self.x = self.x.saturating_sub(1);
342    }
343
344    pub fn tab(&mut self) {
345        self.pending_wrap = false;
346        self.x = ((self.x / 8 + 1) * 8).min(self.cols - 1);
347    }
348
349    pub fn back_tab(&mut self, n: usize) {
350        for _ in 0..n {
351            self.x = self.x.saturating_sub(1) / 8 * 8;
352        }
353        self.pending_wrap = false;
354    }
355
356    /// Absolute move, 0-based, clamped to the screen.
357    pub fn move_to(&mut self, x: usize, y: usize) {
358        self.x = x.min(self.cols - 1);
359        self.y = y.min(self.rows - 1);
360        self.pending_wrap = false;
361    }
362
363    pub fn move_by(&mut self, dx: isize, dy: isize) {
364        let x = (self.x as isize + dx).max(0) as usize;
365        let y = (self.y as isize + dy).max(0) as usize;
366        self.move_to(x, y);
367    }
368
369    pub fn set_scroll_region(&mut self, top: usize, bottom: usize) {
370        let bottom = bottom.min(self.rows - 1);
371        if top < bottom {
372            self.scroll_top = top;
373            self.scroll_bottom = bottom;
374        } else {
375            self.scroll_top = 0;
376            self.scroll_bottom = self.rows - 1;
377        }
378        self.move_to(0, 0);
379    }
380
381    pub fn save_cursor(&mut self) {
382        self.saved = Some(SavedCursor { x: self.x, y: self.y, pen: self.pen });
383    }
384
385    pub fn restore_cursor(&mut self) {
386        if let Some(s) = self.saved {
387            self.move_to(s.x, s.y);
388            self.pen = s.pen;
389        }
390    }
391
392    fn push_scrollback(&mut self, line: Vec<Cell>) {
393        if self.scrollback_limit == 0 {
394            return;
395        }
396        if self.scrollback.len() >= self.scrollback_limit {
397            self.scrollback.pop_front();
398        }
399        self.scrollback.push_back(line);
400    }
401
402    /// Scroll the region up `n` lines. Lines leaving the top of a full-screen
403    /// region go to scrollback.
404    pub fn scroll_up(&mut self, n: usize) {
405        let (top, bottom) = (self.scroll_top, self.scroll_bottom);
406        let n = n.min(bottom - top + 1);
407        if top == 0 && bottom == self.rows - 1 {
408            for y in 0..n {
409                let line = self.row(y).to_vec();
410                self.push_scrollback(line);
411            }
412        }
413        let c = self.cols;
414        self.cells.copy_within((top + n) * c..(bottom + 1) * c, top * c);
415        let blank = self.blank();
416        self.cells[(bottom + 1 - n) * c..(bottom + 1) * c].fill(blank);
417    }
418
419    pub fn scroll_down(&mut self, n: usize) {
420        let (top, bottom) = (self.scroll_top, self.scroll_bottom);
421        let n = n.min(bottom - top + 1);
422        let c = self.cols;
423        self.cells.copy_within(top * c..(bottom + 1 - n) * c, (top + n) * c);
424        let blank = self.blank();
425        self.cells[top * c..(top + n) * c].fill(blank);
426    }
427
428    /// ED: 0 = cursor to end, 1 = start to cursor, 2/3 = whole screen.
429    ///
430    /// Clearing the whole screen saves its content to scrollback first, so
431    /// BBS screens that are wiped between menus can still be reviewed, and,
432    /// like ANSI.SYS, homes the cursor.
433    pub fn erase_display(&mut self, mode: u32) {
434        let blank = self.blank();
435        let pos = self.y * self.cols + self.x;
436        match mode {
437            0 => self.cells[pos..].fill(blank),
438            1 => self.cells[..=pos].fill(blank),
439            _ => {
440                let used = (0..self.rows).rev().find(|&y| self.row(y).iter().any(|c| !c.is_blank()));
441                if let Some(last) = used {
442                    for y in 0..=last {
443                        let line = self.row(y).to_vec();
444                        self.push_scrollback(line);
445                    }
446                }
447                self.cells.fill(blank);
448                self.move_to(0, 0);
449            }
450        }
451        self.pending_wrap = false;
452    }
453
454    /// EL: 0 = cursor to end of line, 1 = start to cursor, 2 = whole line.
455    pub fn erase_line(&mut self, mode: u32) {
456        let blank = self.blank();
457        let start = self.y * self.cols;
458        let (a, b) = match mode {
459            0 => (self.x, self.cols),
460            1 => (0, self.x + 1),
461            _ => (0, self.cols),
462        };
463        self.cells[start + a..start + b].fill(blank);
464        self.pending_wrap = false;
465    }
466
467    /// ECH: blank `n` cells from the cursor without moving it.
468    pub fn erase_chars(&mut self, n: usize) {
469        let blank = self.blank();
470        let start = self.y * self.cols;
471        let end = (self.x + n).min(self.cols);
472        self.cells[start + self.x..start + end].fill(blank);
473    }
474
475    /// ICH: insert `n` blanks at the cursor, shifting the rest of the line right.
476    pub fn insert_chars(&mut self, n: usize) {
477        let blank = self.blank();
478        let start = self.y * self.cols;
479        let n = n.min(self.cols - self.x);
480        let line = &mut self.cells[start..start + self.cols];
481        line.copy_within(self.x..self.cols - n, self.x + n);
482        line[self.x..self.x + n].fill(blank);
483    }
484
485    /// DCH: delete `n` cells at the cursor, shifting the rest of the line left.
486    pub fn delete_chars(&mut self, n: usize) {
487        let blank = self.blank();
488        let start = self.y * self.cols;
489        let n = n.min(self.cols - self.x);
490        let line = &mut self.cells[start..start + self.cols];
491        line.copy_within(self.x + n.., self.x);
492        let cols = self.cols;
493        line[cols - n..].fill(blank);
494    }
495
496    /// IL: insert `n` lines at the cursor row (within the scroll region).
497    pub fn insert_lines(&mut self, n: usize) {
498        if self.y < self.scroll_top || self.y > self.scroll_bottom {
499            return;
500        }
501        let top = self.scroll_top;
502        self.scroll_top = self.y;
503        self.scroll_down(n);
504        self.scroll_top = top;
505        self.x = 0;
506    }
507
508    /// DL: delete `n` lines at the cursor row (within the scroll region).
509    pub fn delete_lines(&mut self, n: usize) {
510        if self.y < self.scroll_top || self.y > self.scroll_bottom {
511            return;
512        }
513        let (top, bottom) = (self.scroll_top, self.scroll_bottom);
514        // Temporarily narrow the region so scroll_up never feeds scrollback.
515        self.scroll_top = self.y;
516        let n = n.min(bottom - self.y + 1);
517        let c = self.cols;
518        self.cells.copy_within((self.y + n) * c..(bottom + 1) * c, self.y * c);
519        let blank = self.blank();
520        self.cells[(bottom + 1 - n) * c..(bottom + 1) * c].fill(blank);
521        self.scroll_top = top;
522        self.x = 0;
523    }
524
525    /// Write a string of CP437 glyphs directly at a position without moving
526    /// the cursor; used by the built-in text-mode UI.
527    pub fn draw_text(&mut self, x: usize, y: usize, text: &[u8], fg: Color, bg: Color) {
528        for (i, &ch) in text.iter().enumerate() {
529            if let Some(c) = self.cell_mut(x + i, y) {
530                *c = Cell::new(ch, fg, bg);
531            }
532        }
533    }
534
535    pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize, cell: Cell) {
536        for yy in y..(y + h).min(self.rows) {
537            for xx in x..(x + w).min(self.cols) {
538                self.cells[yy * self.cols + xx] = cell;
539            }
540        }
541    }
542
543    /// The size of a cell in pixels, for pixel graphics: an 8x16 VGA font,
544    /// or 8x8 on screens of 43 rows or more (as BadTerm draws them).
545    pub fn cell_px(&self) -> (usize, usize) {
546        (8, if self.rows >= 43 { 8 } else { 16 })
547    }
548
549    /// The screen's size in pixels, for pixel graphics.
550    pub fn size_px(&self) -> (usize, usize) {
551        let (w, h) = self.cell_px();
552        (self.cols * w, self.rows * h)
553    }
554
555    /// Put a picture on the screen with its top left at the cursor's cell,
556    /// as sixel does: with `scrolling`, the screen scrolls to make room
557    /// and the cursor ends on the row below the picture's last full band,
558    /// at the column it started in; without, it goes at the top left of
559    /// the screen, what doesn't fit is cut off and the cursor stays put.
560    pub fn place_image(&mut self, img: &crate::sixel::Image, scrolling: bool) {
561        if img.width == 0 || img.height == 0 {
562            return;
563        }
564        let (tw, th) = self.cell_px();
565        let cx = self.x;
566        let rows = img.height.div_ceil(th);
567        let cols = img.width.div_ceil(tw);
568        if !scrolling {
569            for r in 0..rows.min(self.rows) {
570                self.tile_row(img, r, 0, r, cols);
571            }
572            return;
573        }
574        self.pending_wrap = false;
575        let last = img.height / th;
576        for r in 0..=last {
577            if r > 0 {
578                self.line_feed();
579            }
580            if r < rows {
581                let y = self.y;
582                self.tile_row(img, r, cx, y, cols);
583            }
584        }
585        self.x = cx;
586    }
587
588    /// Row `r` of a picture's tiles onto screen row `y`, from column `x0`.
589    fn tile_row(&mut self, img: &crate::sixel::Image, r: usize, x0: usize, y: usize, cols: usize) {
590        let (tw, th) = self.cell_px();
591        let (left, top) = ((x0 * tw) as isize, (y * th) as isize);
592        let rect = (left, top, cols * tw, th);
593        self.draw_pixels(rect, |px, py| {
594            let (ix, iy) = ((px - left) as usize, (py - top) as usize + r * th);
595            if ix < img.width && iy < img.height { img.pixels[iy * img.width + ix] } else { 0 }
596        });
597    }
598
599    /// Draw pixels over the screen in `rect` (x, y, w, h, in screen pixels;
600    /// it may go off the screen, and that part is clipped): `pixel(x, y)`
601    /// is each one's color, 0xFFRRGGBB, or 0 to leave it as it is.
602    pub fn draw_pixels(&mut self, rect: (isize, isize, usize, usize), pixel: impl Fn(isize, isize) -> u32) {
603        let (tw, th) = self.cell_px();
604        let (sw, sh) = self.size_px();
605        let (x0, y0) = (rect.0.max(0) as usize, rect.1.max(0) as usize);
606        let x1 = ((rect.0 + rect.2 as isize).max(0) as usize).min(sw);
607        let y1 = ((rect.1 + rect.3 as isize).max(0) as usize).min(sh);
608        if x0 >= x1 || y0 >= y1 {
609            return;
610        }
611        for cy in y0 / th..y1.div_ceil(th) {
612            for cx in x0 / tw..x1.div_ceil(tw) {
613                let idx = cy * self.cols + cx;
614                let mut px = match self.tiles.get(self.cells[idx].tile) {
615                    Some(t) if (t.width, t.height) == (tw, th) => t.pixels.to_vec(),
616                    _ => vec![0; tw * th],
617                };
618                let mut any = false;
619                for ty in 0..th {
620                    let y = cy * th + ty;
621                    if y < y0 || y >= y1 {
622                        continue;
623                    }
624                    for tx in 0..tw {
625                        let x = cx * tw + tx;
626                        if x < x0 || x >= x1 {
627                            continue;
628                        }
629                        let p = pixel(x as isize, y as isize);
630                        if p != 0 {
631                            px[ty * tw + tx] = p;
632                            any = true;
633                        }
634                    }
635                }
636                if !any {
637                    continue;
638                }
639                if self.tiles.full() {
640                    let mut used = std::collections::HashSet::new();
641                    used.extend(self.cells.iter().map(|c| c.tile));
642                    used.extend(self.scrollback.iter().flatten().map(|c| c.tile));
643                    self.tiles.collect(&used);
644                }
645                self.cells[idx].tile = self.tiles.add(Tile { width: tw, height: th, pixels: px.into() });
646            }
647        }
648    }
649
650    /// The color of a screen pixel, as pixel operations see it: the
651    /// cell's pixel graphics, or else its background (text isn't drawn
652    /// here, so it reads as background).
653    pub fn pixel_at(&self, x: usize, y: usize) -> crate::palette::Rgb {
654        let (tw, th) = self.cell_px();
655        let Some(cell) = self.cells.get((y / th) * self.cols + x / tw).filter(|_| x / tw < self.cols) else {
656            return 0;
657        };
658        if let Some(t) = self.tiles.get(cell.tile) {
659            let p = t.pixels.get((y % th) * t.width + x % tw).copied().unwrap_or(0);
660            if p != 0 {
661                return p & 0x00FF_FFFF;
662            }
663        }
664        cell.resolve_with(self.ice_colors, &self.look).1
665    }
666
667    /// Plain-text contents of a row, trailing blanks trimmed.
668    pub fn row_text(row: &[Cell]) -> String {
669        let s: String = row.iter().map(|c| crate::cp437::to_unicode(c.ch.max(b' '))).collect();
670        s.trim_end().to_string()
671    }
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    fn text(s: &Screen, y: usize) -> String {
679        Screen::row_text(s.row(y))
680    }
681
682    #[test]
683    fn delayed_wrap_at_last_column() {
684        let mut s = Screen::new(4, 3, 10);
685        for &b in b"abcd" {
686            s.put(b);
687        }
688        assert_eq!(s.cursor(), (3, 0));
689        s.carriage_return();
690        s.line_feed();
691        s.put(b'e');
692        assert_eq!(text(&s, 0), "abcd");
693        assert_eq!(text(&s, 1), "e");
694    }
695
696    #[test]
697    fn scrolling_feeds_scrollback() {
698        let mut s = Screen::new(4, 2, 10);
699        for &b in b"ab" {
700            s.put(b);
701            s.carriage_return();
702            s.line_feed();
703        }
704        assert_eq!(s.scrollback_len(), 1);
705        assert_eq!(Screen::row_text(s.scrollback_line(0)), "a");
706        assert_eq!(text(&s, 0), "b");
707    }
708
709    #[test]
710    fn clear_screen_saves_to_scrollback() {
711        let mut s = Screen::new(4, 3, 10);
712        s.put(b'x');
713        s.erase_display(2);
714        assert_eq!(s.scrollback_len(), 1);
715        assert_eq!(s.cursor(), (0, 0));
716    }
717
718    #[test]
719    fn insert_delete_chars() {
720        let mut s = Screen::new(5, 1, 0);
721        for &b in b"abcde" {
722            s.put(b);
723        }
724        s.move_to(1, 0);
725        s.delete_chars(2);
726        assert_eq!(text(&s, 0), "ade");
727        s.insert_chars(1);
728        assert_eq!(text(&s, 0), "a de");
729    }
730
731    #[test]
732    fn ice_colors_resolve() {
733        let c = Cell { ch: b'A', fg: Color::Pc(1), bg: Color::Pc(4), flags: flags::BOLD | flags::BLINK, tile: 0 };
734        assert_eq!(c.resolve(false), (VGA[9], VGA[4], true));
735        assert_eq!(c.resolve(true), (VGA[9], VGA[12], false));
736    }
737}
738
739/// One cell's pixel graphics: 0xFFRRGGBB pixels, 0 where the cell's own
740/// text and background show through.
741#[derive(Clone, Debug, PartialEq, Eq)]
742pub struct Tile {
743    pub width: usize,
744    pub height: usize,
745    pub pixels: Box<[u32]>,
746}
747
748/// The tiles the screen's cells (and scrollback) show, by ID. Ones no cell
749/// shows any more are reused once there are many.
750#[derive(Debug, Default)]
751pub struct Tiles {
752    store: Vec<Option<Tile>>,
753    free: Vec<u32>,
754    /// Look for unused tiles once there are this many.
755    next_collect: usize,
756}
757
758impl Tiles {
759    pub fn get(&self, id: u32) -> Option<&Tile> {
760        id.checked_sub(1).and_then(|i| self.store.get(i as usize)).and_then(|t| t.as_ref())
761    }
762
763    fn full(&self) -> bool {
764        self.free.is_empty() && self.store.len() >= self.next_collect.max(4096)
765    }
766
767    /// Free every tile not in `used`.
768    fn collect(&mut self, used: &std::collections::HashSet<u32>) {
769        self.free.clear();
770        for (i, t) in self.store.iter_mut().enumerate() {
771            let id = i as u32 + 1;
772            if !used.contains(&id) {
773                *t = None;
774                self.free.push(id);
775            }
776        }
777        self.next_collect = (self.store.len() - self.free.len()) * 2;
778    }
779
780    fn add(&mut self, tile: Tile) -> u32 {
781        match self.free.pop() {
782            Some(id) => {
783                self.store[id as usize - 1] = Some(tile);
784                id
785            }
786            None => {
787                self.store.push(Some(tile));
788                self.store.len() as u32
789            }
790        }
791    }
792}