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