Skip to main content

badterm_core/
rip.rs

1//! RIPscrip 1.54 graphics: the vector scenes, buttons and clickable mouse
2//! regions of RIPterm-era BBSes, drawn on a 640x350 EGA canvas under the
3//! 80x43 text screen.
4//!
5//! A RIP line starts with `!|` at the beginning of a line and holds commands
6//! separated by `|`, each a command letter (with a level digit before it
7//! for level 1 commands) and its parameters, mostly two-digit base-36
8//! "MegaNums". A backslash at the end of a line continues it on the next;
9//! `\|`, `\\` and `\!` are literal characters in text.
10//!
11//! Drawing follows the Borland BGI the format was built on: colors are
12//! indices into a 16-entry palette of the EGA's 64 colors, lines have a
13//! style and thickness, areas are filled with one of BGI's patterns, and
14//! circles are corrected for the EGA's tall pixels. Text uses the 8x8 font,
15//! scaled up for larger sizes; BGI's vector fonts (Borland's own files) are
16//! drawn in it too.
17//!
18//! Left out: icon files (`1I`, `1W`), scene files and file queries (`1R`,
19//! `1F`), variables (`1D`) and RIP's block-mode file transfers.
20
21use crate::palette::Rgb;
22use crate::screen::{Cell, Screen};
23
24pub const WIDTH: usize = 640;
25pub const HEIGHT: usize = 350;
26/// The text screen over the canvas: 80x43 in the 8x8 font.
27pub const COLS: usize = 80;
28pub const ROWS: usize = 43;
29
30/// What a RIP terminal answers to `ESC [ !`: RIPscrip, version 1.54.
31pub const VERSION_REPLY: &[u8] = b"RIPSCRIP015400";
32
33/// The longest RIP line kept, continuations included.
34const MAX_LINE: usize = 64 * 1024;
35
36static FONT_8X8: &[u8; 2048] = include_bytes!("../assets/vga_8x8.bin");
37
38/// The EGA's default palette: indices into its 64 colors.
39const DEFAULT_PALETTE: [u8; 16] = [0, 1, 2, 3, 4, 5, 20, 7, 56, 57, 58, 59, 60, 61, 62, 63];
40
41/// BGI's fill patterns, 8x8, most significant bit leftmost.
42const FILL_PATTERNS: [[u8; 8]; 12] = [
43    [0x00; 8],
44    [0xFF; 8],
45    [0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00],
46    [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80],
47    [0xE0, 0xC1, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70],
48    [0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x87, 0xC3, 0xE1],
49    [0xA5, 0xD2, 0x69, 0xB4, 0x5A, 0x2D, 0x96, 0x4B],
50    [0xFF, 0x88, 0x88, 0x88, 0xFF, 0x88, 0x88, 0x88],
51    [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81],
52    [0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33, 0xCC, 0x33],
53    [0x80, 0x00, 0x08, 0x00, 0x80, 0x00, 0x08, 0x00],
54    [0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00],
55];
56
57/// BGI's line styles: solid, dotted, center and dashed.
58const LINE_STYLES: [u16; 4] = [0xFFFF, 0xCCCC, 0xFC78, 0xF8F8];
59
60/// The EGA draws circles this much flatter, so they look round on a 4:3
61/// screen of 640x350 (BGI's aspect ratio for it).
62const ASPECT: f64 = 0.775;
63
64/// One of the EGA's 64 colors (bits: secondary R G B, primary R G B).
65pub fn ega_rgb(c: u8) -> Rgb {
66    let level = |hi: u8, lo: u8| (((c >> hi) & 1) * 0xAA + ((c >> lo) & 1) * 0x55) as u32;
67    (level(2, 5) << 16) | (level(1, 4) << 8) | level(0, 3)
68}
69
70/// A clickable area: when clicked, `text` is sent to the BBS.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct MouseRegion {
73    pub x0: usize,
74    pub y0: usize,
75    pub x1: usize,
76    pub y1: usize,
77    /// Show the area inverted while it is clicked.
78    pub invert: bool,
79    pub text: Vec<u8>,
80}
81
82#[derive(Clone, Copy, Debug, Default)]
83struct ButtonStyle {
84    width: usize,
85    height: usize,
86    bevel: usize,
87    dfore: u8,
88    dback: u8,
89    bright: u8,
90    dark: u8,
91    surface: u8,
92}
93
94#[derive(Clone, Copy, Debug)]
95struct Viewport {
96    x0: usize,
97    y0: usize,
98    x1: usize,
99    y1: usize,
100}
101
102const FULL: Viewport = Viewport { x0: 0, y0: 0, x1: WIDTH - 1, y1: HEIGHT - 1 };
103
104/// A text block being laid out (`1T` ... `1E`).
105struct TextBlock {
106    x0: i32,
107    x1: i32,
108    y: i32,
109}
110
111pub struct Rip {
112    pixels: Vec<u8>,
113    /// Palette entry -> EGA color.
114    palette: [u8; 16],
115    color: u8,
116    fill_color: u8,
117    fill: [u8; 8],
118    line_style: u16,
119    thick: bool,
120    xor: bool,
121    view: Viewport,
122    /// The drawing position (for text), relative to the viewport.
123    pos: (i32, i32),
124    font_size: usize,
125    vertical: bool,
126    button: ButtonStyle,
127    regions: Vec<MouseRegion>,
128    clipboard: Option<(usize, usize, Vec<u8>)>,
129    text_block: Option<TextBlock>,
130    /// The text window, in text cells (x0, y0, x1, y1).
131    text_window: (usize, usize, usize, usize),
132    /// The RIP line being read, from its `!`.
133    line: Option<Vec<u8>>,
134    /// A RIP line just ended with CR: its LF goes too.
135    eat_lf: bool,
136    /// Changed since the front end last looked.
137    pub dirty: bool,
138}
139
140/// What [`Rip::byte`] did with a byte.
141pub enum Feed {
142    /// Not RIP: handle it as text.
143    Pass,
144    /// Taken.
145    Done,
146    /// It turned out not to be a RIP line: show these bytes as text.
147    Text(Vec<u8>),
148}
149
150impl Default for Rip {
151    fn default() -> Self {
152        Rip {
153            pixels: vec![0; WIDTH * HEIGHT],
154            palette: DEFAULT_PALETTE,
155            color: 15,
156            fill_color: 15,
157            fill: FILL_PATTERNS[1],
158            line_style: 0xFFFF,
159            thick: false,
160            xor: false,
161            view: FULL,
162            pos: (0, 0),
163            font_size: 1,
164            vertical: false,
165            button: ButtonStyle::default(),
166            regions: Vec::new(),
167            clipboard: None,
168            text_block: None,
169            text_window: (0, 0, COLS - 1, ROWS - 1),
170            line: None,
171            eat_lf: false,
172            dirty: true,
173        }
174    }
175}
176
177impl Rip {
178    /// The canvas: `WIDTH` x `HEIGHT` palette indices.
179    pub fn pixels(&self) -> &[u8] {
180        &self.pixels
181    }
182
183    /// The colors of the 16 palette entries.
184    pub fn palette(&self) -> [Rgb; 16] {
185        self.palette.map(ega_rgb)
186    }
187
188    pub fn regions(&self) -> &[MouseRegion] {
189        &self.regions
190    }
191
192    /// What clicking the canvas at (x, y) sends, if it is in a mouse region
193    /// (the last defined wins, as they are drawn over each other).
194    pub fn click(&self, x: usize, y: usize) -> Option<Vec<u8>> {
195        self.regions
196            .iter()
197            .rev()
198            .find(|r| (r.x0..=r.x1).contains(&x) && (r.y0..=r.y1).contains(&y))
199            .map(|r| r.text.clone())
200    }
201
202    /// Take a byte at the text level: `at_line_start` says whether it
203    /// begins a line, where a RIP line may start.
204    pub fn byte(&mut self, s: &mut Screen, b: u8, at_line_start: bool) -> Feed {
205        if std::mem::take(&mut self.eat_lf) && b == b'\n' {
206            return Feed::Done;
207        }
208        let Some(line) = self.line.as_mut() else {
209            if at_line_start && b == b'!' {
210                self.line = Some(vec![b]);
211                return Feed::Done;
212            }
213            return Feed::Pass;
214        };
215        // `!` must be followed by `|`; otherwise it was text.
216        if line.len() == 1 && b != b'|' {
217            let mut text = self.line.take().unwrap_or_default();
218            text.push(b);
219            return Feed::Text(text);
220        }
221        match b {
222            b'\r' | b'\n' => {
223                // A backslash before the end of the line continues it.
224                if line.last() == Some(&b'\\') && !line.ends_with(b"\\\\") {
225                    line.pop();
226                    line.push(0);
227                    return Feed::Done;
228                }
229                if line.last() == Some(&0) {
230                    // The LF of a continued line's CR LF.
231                    return Feed::Done;
232                }
233                let line = self.line.take().unwrap_or_default();
234                self.run_line(s, &line);
235                // The line's end is not shown: RIP lines are invisible.
236                self.eat_lf = b == b'\r';
237                Feed::Done
238            }
239            _ => {
240                if line.last() == Some(&0) {
241                    line.pop();
242                }
243                if line.len() < MAX_LINE {
244                    line.push(b);
245                }
246                Feed::Done
247            }
248        }
249    }
250
251    fn run_line(&mut self, s: &mut Screen, line: &[u8]) {
252        // Split on unescaped bars; the first piece is the `!`.
253        let mut cmds: Vec<Vec<u8>> = vec![Vec::new()];
254        let mut i = 1;
255        while i < line.len() {
256            match line[i] {
257                b'\\' if i + 1 < line.len() => {
258                    cmds.last_mut().expect("never empty").push(line[i + 1]);
259                    i += 1;
260                }
261                b'|' => cmds.push(Vec::new()),
262                c => cmds.last_mut().expect("never empty").push(c),
263            }
264            i += 1;
265        }
266        for cmd in cmds.iter().filter(|c| !c.is_empty()) {
267            self.command(s, cmd);
268        }
269        self.dirty = true;
270    }
271
272    fn command(&mut self, s: &mut Screen, cmd: &[u8]) {
273        let (level, op, args) = match cmd {
274            [d @ b'1'..=b'9', op, args @ ..] => (d - b'0', *op, args),
275            [op, args @ ..] => (0, *op, args),
276            [] => return,
277        };
278        let mut a = Args(args);
279        match (level, op) {
280            (0, b'w') => {
281                let (x0, y0, x1, y1) = (a.n2(), a.n2(), a.n2(), a.n2());
282                let (x1, y1) = (x1.clamp(x0, COLS - 1), y1.clamp(y0, ROWS - 1));
283                self.text_window = (x0.min(COLS - 1), y0.min(ROWS - 1), x1, y1);
284                s.set_scroll_region(self.text_window.1, self.text_window.3);
285                s.move_to(self.text_window.0, self.text_window.1);
286            }
287            (0, b'v') => {
288                let (x0, y0, x1, y1) = (a.n2(), a.n2(), a.n2(), a.n2());
289                self.view = Viewport {
290                    x0: x0.min(WIDTH - 1),
291                    y0: y0.min(HEIGHT - 1),
292                    x1: x1.clamp(x0.min(WIDTH - 1), WIDTH - 1),
293                    y1: y1.clamp(y0.min(HEIGHT - 1), HEIGHT - 1),
294                };
295            }
296            (0, b'*') => self.reset(s),
297            (0, b'e') => {
298                let (x0, y0, x1, y1) = self.text_window;
299                s.fill_rect(x0, y0, x1 - x0 + 1, y1 - y0 + 1, Cell { ch: b' ', ..s.pen });
300                s.move_to(x0, y0);
301            }
302            (0, b'E') => {
303                let v = self.view;
304                for y in v.y0..=v.y1 {
305                    self.pixels[y * WIDTH + v.x0..=y * WIDTH + v.x1].fill(0);
306                }
307            }
308            (0, b'g') => {
309                let (x, y) = (a.n2(), a.n2());
310                s.move_to(self.text_window.0 + x, self.text_window.1 + y);
311            }
312            (0, b'H') => s.move_to(self.text_window.0, self.text_window.1),
313            (0, b'>') => s.erase_line(0),
314            (0, b'c') => self.color = a.n2() as u8 & 15,
315            (0, b'Q') => {
316                for i in 0..16 {
317                    self.palette[i] = a.n2() as u8 & 63;
318                }
319            }
320            (0, b'a') => {
321                let (i, c) = (a.n2(), a.n2());
322                self.palette[i & 15] = c as u8 & 63;
323            }
324            (0, b'W') => self.xor = a.n2() == 1,
325            (0, b'm') => self.pos = (a.i2(), a.i2()),
326            (0, b'T') => {
327                let text = a.rest().to_vec();
328                self.text(&text);
329            }
330            (0, b'@') => {
331                self.pos = (a.i2(), a.i2());
332                let text = a.rest().to_vec();
333                self.text(&text);
334            }
335            (0, b'Y') => {
336                let (_font, dir, size) = (a.n2(), a.n2(), a.n2());
337                self.vertical = dir == 1;
338                self.font_size = size.clamp(1, 10);
339            }
340            (0, b'X') => {
341                let (x, y) = (a.i2(), a.i2());
342                self.plot(x, y, self.color);
343            }
344            (0, b'L') => {
345                let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
346                self.line(x0, y0, x1, y1);
347            }
348            (0, b'R') => {
349                let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
350                self.polyline(&[(x0, y0), (x1, y0), (x1, y1), (x0, y1)], true);
351            }
352            (0, b'B') => {
353                let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
354                self.bar(x0, y0, x1, y1);
355            }
356            (0, b'C') => {
357                let (x, y, r) = (a.i2(), a.i2(), a.i2());
358                let pts = arc_points(x, y, 0, 360, r as f64, r as f64 * ASPECT);
359                self.polyline(&pts, false);
360            }
361            (0, b'O') => {
362                let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
363                let pts = arc_points(x, y, st, end, xr as f64, yr as f64);
364                self.polyline(&pts, false);
365            }
366            (0, b'o') => {
367                let (x, y, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2());
368                let pts = arc_points(x, y, 0, 360, xr as f64, yr as f64);
369                self.fill_polygon(&pts);
370                self.polyline(&pts, false);
371            }
372            (0, b'A') => {
373                let (x, y, st, end, r) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
374                let pts = arc_points(x, y, st, end, r as f64, r as f64 * ASPECT);
375                self.polyline(&pts, false);
376            }
377            (0, b'V') => {
378                let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
379                let pts = arc_points(x, y, st, end, xr as f64, yr as f64);
380                self.polyline(&pts, false);
381            }
382            (0, b'I') => {
383                let (x, y, st, end, r) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
384                self.pie(x, y, st, end, r as f64, r as f64 * ASPECT);
385            }
386            (0, b'i') => {
387                let (x, y, st, end, xr, yr) = (a.i2(), a.i2(), a.i2(), a.i2(), a.i2(), a.i2());
388                self.pie(x, y, st, end, xr as f64, yr as f64);
389            }
390            (0, b'Z') => {
391                let p: Vec<(f64, f64)> = (0..4).map(|_| (a.i2() as f64, a.i2() as f64)).collect();
392                let n = a.n2().max(1);
393                let pts: Vec<(i32, i32)> = (0..=n)
394                    .map(|i| {
395                        let t = i as f64 / n as f64;
396                        let u = 1.0 - t;
397                        let (b0, b1, b2, b3) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
398                        let x = b0 * p[0].0 + b1 * p[1].0 + b2 * p[2].0 + b3 * p[3].0;
399                        let y = b0 * p[0].1 + b1 * p[1].1 + b2 * p[2].1 + b3 * p[3].1;
400                        (x.round() as i32, y.round() as i32)
401                    })
402                    .collect();
403                self.polyline(&pts, false);
404            }
405            (0, b'P' | b'p' | b'l') => {
406                let n = a.n2();
407                let pts: Vec<(i32, i32)> = (0..n).map(|_| (a.i2(), a.i2())).collect();
408                if op == b'p' {
409                    self.fill_polygon(&pts);
410                }
411                self.polyline(&pts, op != b'l');
412            }
413            (0, b'F') => {
414                let (x, y, border) = (a.i2(), a.i2(), a.n2() as u8);
415                self.flood(x, y, border & 15);
416            }
417            (0, b'=') => {
418                let (style, user, thick) = (a.n2(), a.n4(), a.n2());
419                self.line_style = LINE_STYLES.get(style).copied().unwrap_or(user as u16);
420                self.thick = thick >= 3;
421            }
422            (0, b'S') => {
423                let (pattern, color) = (a.n2(), a.n2());
424                self.fill = FILL_PATTERNS.get(pattern).copied().unwrap_or(FILL_PATTERNS[1]);
425                self.fill_color = color as u8 & 15;
426            }
427            (0, b's') => {
428                for row in self.fill.iter_mut() {
429                    *row = a.n2() as u8;
430                }
431                self.fill_color = a.n2() as u8 & 15;
432            }
433            (0, b'#') => {}
434            (1, b'M') => {
435                let (_num, x0, y0, x1, y1, clk, _clr) = (a.n2(), a.n2(), a.n2(), a.n2(), a.n2(), a.n1(), a.n1());
436                a.skip(5);
437                let text = host_command(a.rest());
438                self.region(x0, y0, x1, y1, clk == 1, text);
439            }
440            (1, b'K') => self.regions.clear(),
441            (1, b'T') => {
442                let (x0, y0, x1) = (a.i2(), a.i2(), a.i2());
443                self.text_block = Some(TextBlock { x0, x1, y: y0 });
444            }
445            (1, b't') => {
446                let _justify = a.n1();
447                let text = a.rest().to_vec();
448                if let Some(tb) = self.text_block.as_mut() {
449                    let (x0, x1, y) = (tb.x0, tb.x1, tb.y);
450                    tb.y += 8 * self.font_size as i32;
451                    let room = ((x1 - x0).max(0) as usize / (8 * self.font_size)).max(1);
452                    self.pos = (x0, y);
453                    self.text(&text[..text.len().min(room)]);
454                }
455            }
456            (1, b'E') => self.text_block = None,
457            (1, b'C') => {
458                let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
459                self.copy_to_clipboard(x0, y0, x1, y1);
460            }
461            (1, b'P') => {
462                let (x, y, mode) = (a.i2(), a.i2(), a.n2());
463                self.paste(x, y, mode);
464            }
465            (1, b'G') => {
466                let (x0, y0, x1, y1) = (a.i2(), a.i2(), a.i2(), a.i2());
467                a.skip(2);
468                let dest = a.i2();
469                self.copy_rows(x0, y0, x1, y1, dest);
470            }
471            (1, b'B') => {
472                self.button = ButtonStyle {
473                    width: a.n2(),
474                    height: a.n2(),
475                    bevel: {
476                        // The orientation and flags (icons, hot keys,
477                        // radio groups) are not used.
478                        a.skip(6);
479                        a.n2()
480                    },
481                    dfore: a.n2() as u8 & 15,
482                    dback: a.n2() as u8 & 15,
483                    bright: a.n2() as u8 & 15,
484                    dark: a.n2() as u8 & 15,
485                    surface: a.n2() as u8 & 15,
486                };
487            }
488            (1, b'U') => {
489                let (x0, y0, x1, y1, _hotkey, _flags) = (a.i2(), a.i2(), a.i2(), a.i2(), a.n2(), a.n1());
490                a.skip(1);
491                let text = a.rest().to_vec();
492                self.draw_button(x0, y0, x1, y1, &text);
493            }
494            _ => log::debug!("unhandled RIP command {level}{}", op as char),
495        }
496    }
497
498    /// `RIP_RESET_WINDOWS`: everything back as it starts.
499    fn reset(&mut self, s: &mut Screen) {
500        *self = Rip { line: self.line.take(), eat_lf: self.eat_lf, ..Rip::default() };
501        s.set_scroll_region(0, s.rows() - 1);
502        s.erase_display(2);
503        s.move_to(0, 0);
504    }
505
506    /// Set a pixel, at viewport coordinates, clipped to the viewport.
507    fn plot(&mut self, x: i32, y: i32, c: u8) {
508        let v = self.view;
509        let (x, y) = (x + v.x0 as i32, y + v.y0 as i32);
510        if x < v.x0 as i32 || y < v.y0 as i32 || x > v.x1 as i32 || y > v.y1 as i32 {
511            return;
512        }
513        let p = &mut self.pixels[y as usize * WIDTH + x as usize];
514        *p = if self.xor { *p ^ c } else { c };
515    }
516
517    fn get(&self, x: i32, y: i32) -> Option<u8> {
518        let v = self.view;
519        let (x, y) = (x + v.x0 as i32, y + v.y0 as i32);
520        (x >= v.x0 as i32 && y >= v.y0 as i32 && x <= v.x1 as i32 && y <= v.y1 as i32)
521            .then(|| self.pixels[y as usize * WIDTH + x as usize])
522    }
523
524    /// A pixel of the fill pattern: the fill color where its bit is set,
525    /// the background (0) elsewhere.
526    fn fill_at(&mut self, x: i32, y: i32) {
527        let v = self.view;
528        let bit = self.fill[(y + v.y0 as i32).rem_euclid(8) as usize] & (0x80 >> (x + v.x0 as i32).rem_euclid(8));
529        let c = if bit != 0 { self.fill_color } else { 0 };
530        self.plot(x, y, c);
531    }
532
533    /// A line in the line style, three pixels wide when thick.
534    fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
535        let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
536        let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
537        let steep = -dy > dx;
538        let (mut x, mut y, mut err) = (x0, y0, dx + dy);
539        let mut n = 0u32;
540        loop {
541            if self.line_style & (0x8000 >> (n % 16)) != 0 {
542                self.plot(x, y, self.color);
543                if self.thick {
544                    let (ox, oy) = if steep { (1, 0) } else { (0, 1) };
545                    self.plot(x - ox, y - oy, self.color);
546                    self.plot(x + ox, y + oy, self.color);
547                }
548            }
549            n += 1;
550            if x == x1 && y == y1 {
551                break;
552            }
553            let e2 = 2 * err;
554            if e2 >= dy {
555                err += dy;
556                x += sx;
557            }
558            if e2 <= dx {
559                err += dx;
560                y += sy;
561            }
562        }
563    }
564
565    fn polyline(&mut self, pts: &[(i32, i32)], closed: bool) {
566        for w in pts.windows(2) {
567            self.line(w[0].0, w[0].1, w[1].0, w[1].1);
568        }
569        if closed && pts.len() > 2 {
570            let (a, b) = (pts[pts.len() - 1], pts[0]);
571            self.line(a.0, a.1, b.0, b.1);
572        }
573    }
574
575    fn bar(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
576        for y in y0.min(y1)..=y0.max(y1) {
577            for x in x0.min(x1)..=x0.max(x1) {
578                self.fill_at(x, y);
579            }
580        }
581    }
582
583    /// Fill a polygon (even-odd, by scan lines) with the fill pattern.
584    fn fill_polygon(&mut self, pts: &[(i32, i32)]) {
585        if pts.len() < 3 {
586            return;
587        }
588        let (top, bottom) = pts.iter().fold((i32::MAX, i32::MIN), |(t, b), &(_, y)| (t.min(y), b.max(y)));
589        for y in top..=bottom {
590            let yc = y as f64 + 0.5;
591            let mut xs: Vec<f64> = Vec::new();
592            for i in 0..pts.len() {
593                let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
594                let (ay, by) = (a.1 as f64, b.1 as f64);
595                if (ay <= yc) != (by <= yc) {
596                    xs.push(a.0 as f64 + (yc - ay) / (by - ay) * (b.0 - a.0) as f64);
597                }
598            }
599            xs.sort_by(f64::total_cmp);
600            for pair in xs.chunks(2) {
601                if let [l, r] = pair {
602                    for x in l.round() as i32..=r.round() as i32 {
603                        self.fill_at(x, y);
604                    }
605                }
606            }
607        }
608    }
609
610    fn pie(&mut self, x: i32, y: i32, st: i32, end: i32, xr: f64, yr: f64) {
611        let mut pts = vec![(x, y)];
612        pts.extend(arc_points(x, y, st, end, xr, yr));
613        self.fill_polygon(&pts);
614        self.polyline(&pts, true);
615    }
616
617    /// Flood the area around (x, y) up to the border color with the fill
618    /// pattern.
619    fn flood(&mut self, x: i32, y: i32, border: u8) {
620        let v = self.view;
621        let (w, h) = ((v.x1 - v.x0 + 1) as i32, (v.y1 - v.y0 + 1) as i32);
622        if self.get(x, y).is_none_or(|c| c == border) {
623            return;
624        }
625        let mut seen = vec![false; (w * h) as usize];
626        let mut stack = vec![(x, y)];
627        while let Some((px, py)) = stack.pop() {
628            if px < 0 || py < 0 || px >= w || py >= h {
629                continue;
630            }
631            let i = (py * w + px) as usize;
632            if seen[i] || self.get(px, py) == Some(border) {
633                continue;
634            }
635            // Fill this run of the line, then look above and below it.
636            let mut l = px;
637            while l > 0 && !seen[(py * w + l - 1) as usize] && self.get(l - 1, py) != Some(border) {
638                l -= 1;
639            }
640            let mut r = px;
641            while r + 1 < w && !seen[(py * w + r + 1) as usize] && self.get(r + 1, py) != Some(border) {
642                r += 1;
643            }
644            for xx in l..=r {
645                seen[(py * w + xx) as usize] = true;
646                self.fill_at(xx, py);
647                stack.push((xx, py - 1));
648                stack.push((xx, py + 1));
649            }
650        }
651    }
652
653    /// Draw text at the drawing position in the 8x8 font, scaled by the
654    /// font size, leaving the background; moves the position past it.
655    fn text(&mut self, text: &[u8]) {
656        let k = self.font_size as i32;
657        for &ch in text {
658            let (x0, y0) = self.pos;
659            for gy in 0..8 {
660                let bits = FONT_8X8[ch as usize * 8 + gy as usize];
661                for gx in 0..8 {
662                    if bits & (0x80 >> gx) == 0 {
663                        continue;
664                    }
665                    for sy in 0..k {
666                        for sx in 0..k {
667                            // Vertical text runs upwards, rotated.
668                            let (px, py) = if self.vertical {
669                                (x0 + gy * k + sy, y0 - gx * k - sx)
670                            } else {
671                                (x0 + gx * k + sx, y0 + gy * k + sy)
672                            };
673                            self.plot(px, py, self.color);
674                        }
675                    }
676                }
677            }
678            if self.vertical {
679                self.pos.1 -= 8 * k;
680            } else {
681                self.pos.0 += 8 * k;
682            }
683        }
684    }
685
686    fn region(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, invert: bool, text: Vec<u8>) {
687        let (x1, y1) = (x1.max(x0).min(WIDTH - 1), y1.max(y0).min(HEIGHT - 1));
688        self.regions.push(MouseRegion { x0, y0, x1, y1, invert, text });
689    }
690
691    fn copy_to_clipboard(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
692        let (w, h) = ((x1 - x0 + 1).max(0) as usize, (y1 - y0 + 1).max(0) as usize);
693        let data = (0..h as i32)
694            .flat_map(|dy| (0..w as i32).map(move |dx| (dx, dy)))
695            .map(|(dx, dy)| self.get(x0 + dx, y0 + dy).unwrap_or(0))
696            .collect();
697        self.clipboard = Some((w, h, data));
698    }
699
700    /// Put the clipboard at (x, y): copied, XORed, ORed, ANDed or inverted.
701    fn paste(&mut self, x: i32, y: i32, mode: usize) {
702        let Some((w, h, data)) = self.clipboard.clone() else { return };
703        let xor = self.xor;
704        self.xor = false;
705        for dy in 0..h {
706            for dx in 0..w {
707                let (px, py) = (x + dx as i32, y + dy as i32);
708                let (src, dst) = (data[dy * w + dx], self.get(px, py).unwrap_or(0));
709                let c = match mode {
710                    1 => dst ^ src,
711                    2 => dst | src,
712                    3 => dst & src,
713                    4 => !src & 15,
714                    _ => src,
715                };
716                self.plot(px, py, c);
717            }
718        }
719        self.xor = xor;
720    }
721
722    /// `1G`: copy lines y0..=y1 (columns x0..=x1) to start at line `dest`.
723    fn copy_rows(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, dest: i32) {
724        let clipboard = self.clipboard.take();
725        self.copy_to_clipboard(x0, y0, x1, y1);
726        self.paste(x0, dest, 0);
727        self.clipboard = clipboard;
728    }
729
730    /// `1U`: a button in the current style, with a mouse region when it has
731    /// a host command. Its text is `icon<>label<>command`.
732    fn draw_button(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, text: &[u8]) {
733        let st = self.button;
734        let (x1, y1) = if st.width > 0 && st.height > 0 {
735            (x0 + st.width as i32 - 1, y0 + st.height as i32 - 1)
736        } else {
737            (x1, y1)
738        };
739        let mut parts = split_on(text, b"<>");
740        let (_icon, label, command) =
741            (parts.next().unwrap_or_default(), parts.next().unwrap_or_default(), parts.next().unwrap_or_default());
742        let saved = (self.fill, self.fill_color, self.color, self.line_style, self.thick, self.xor);
743        (self.fill, self.line_style, self.thick, self.xor) = (FILL_PATTERNS[1], 0xFFFF, false, false);
744        let b = st.bevel as i32;
745        // The bevel: light along the top and left, dark along the others.
746        if b > 0 {
747            self.fill_color = st.bright;
748            self.fill_polygon(&[(x0 - b, y0 - b), (x1 + b, y0 - b), (x1, y0), (x0, y0), (x0, y1), (x0 - b, y1 + b)]);
749            self.fill_color = st.dark;
750            self.fill_polygon(&[(x1 + b, y0 - b), (x1 + b, y1 + b), (x0 - b, y1 + b), (x0, y1), (x1, y1), (x1, y0)]);
751        }
752        self.fill_color = st.surface;
753        self.bar(x0, y0, x1, y1);
754        // The label, centered, with its shadow one pixel down and right.
755        let k = self.font_size as i32;
756        let (lw, lh) = (label.len() as i32 * 8 * k, 8 * k);
757        let (lx, ly) = (x0 + (x1 - x0 + 1 - lw) / 2, y0 + (y1 - y0 + 1 - lh) / 2);
758        let vertical = std::mem::replace(&mut self.vertical, false);
759        if st.dback != st.surface {
760            self.color = st.dback;
761            self.pos = (lx + 1, ly + 1);
762            self.text(label);
763        }
764        self.color = st.dfore;
765        self.pos = (lx, ly);
766        self.text(label);
767        self.vertical = vertical;
768        (self.fill, self.fill_color, self.color, self.line_style, self.thick, self.xor) = saved;
769        if !command.is_empty() {
770            let v = self.view;
771            let at = |p: i32, o: usize| (p + o as i32).max(0) as usize;
772            self.region(at(x0, v.x0), at(y0, v.y0), at(x1, v.x0), at(y1, v.y0), true, host_command(command));
773        }
774    }
775}
776
777/// Points along an elliptical arc from `st` to `end` degrees, counter-
778/// clockwise from three o'clock as in BGI.
779fn arc_points(cx: i32, cy: i32, st: i32, end: i32, xr: f64, yr: f64) -> Vec<(i32, i32)> {
780    let end = if end <= st { end + 360 } else { end };
781    let steps = ((end - st) as f64 * (xr.max(yr) / 20.0).max(1.0) / 3.0).ceil().clamp(4.0, 720.0) as usize;
782    (0..=steps)
783        .map(|i| {
784            let a = (st as f64 + (end - st) as f64 * i as f64 / steps as f64).to_radians();
785            ((cx as f64 + xr * a.cos()).round() as i32, (cy as f64 - yr * a.sin()).round() as i32)
786        })
787        .collect()
788}
789
790/// A host command's text, with `^M`-style control characters made real.
791fn host_command(text: &[u8]) -> Vec<u8> {
792    let mut out = Vec::with_capacity(text.len());
793    let mut i = 0;
794    while i < text.len() {
795        match (text[i], text.get(i + 1)) {
796            (b'^', Some(&c)) if (b'@'..=b'_').contains(&c.to_ascii_uppercase()) => {
797                out.push(c.to_ascii_uppercase() - b'@');
798                i += 2;
799            }
800            (c, _) => {
801                out.push(c);
802                i += 1;
803            }
804        }
805    }
806    out
807}
808
809fn split_on<'a>(text: &'a [u8], sep: &'a [u8]) -> impl Iterator<Item = &'a [u8]> {
810    let mut rest = Some(text);
811    std::iter::from_fn(move || {
812        let r = rest?;
813        match r.windows(sep.len()).position(|w| w == sep) {
814            Some(i) => {
815                rest = Some(&r[i + sep.len()..]);
816                Some(&r[..i])
817            }
818            None => {
819                rest = None;
820                Some(r)
821            }
822        }
823    })
824}
825
826/// A command's parameters: MegaNums (base 36) and the text after them.
827struct Args<'a>(&'a [u8]);
828
829impl Args<'_> {
830    fn digits(&mut self, n: usize) -> usize {
831        let (head, tail) = self.0.split_at(n.min(self.0.len()));
832        self.0 = tail;
833        head.iter().fold(0, |v, &c| {
834            let d = match c {
835                b'0'..=b'9' => c - b'0',
836                b'A'..=b'Z' => c - b'A' + 10,
837                b'a'..=b'z' => c - b'a' + 10,
838                _ => 0,
839            };
840            v * 36 + d as usize
841        })
842    }
843
844    fn n1(&mut self) -> usize {
845        self.digits(1)
846    }
847
848    fn n2(&mut self) -> usize {
849        self.digits(2)
850    }
851
852    fn n4(&mut self) -> usize {
853        self.digits(4)
854    }
855
856    fn i2(&mut self) -> i32 {
857        self.n2() as i32
858    }
859
860    fn skip(&mut self, n: usize) {
861        self.0 = &self.0[n.min(self.0.len())..];
862    }
863
864    fn rest(&self) -> &[u8] {
865        self.0
866    }
867}
868
869#[cfg(test)]
870mod tests {
871    use super::*;
872    use crate::ansi::{Encoding, Terminal};
873
874    fn term(data: &[u8]) -> Terminal {
875        let mut t = Terminal::new(COLS, ROWS, 0);
876        t.encoding = Encoding::Rip;
877        t.start_emulation();
878        t.feed(data);
879        t
880    }
881
882    fn px(t: &Terminal, x: usize, y: usize) -> u8 {
883        t.rip().expect("RIP is on").pixels()[y * WIDTH + x]
884    }
885
886    #[test]
887    fn megnums_and_escapes() {
888        let mut a = Args(b"0A1Z00hello\\|");
889        assert_eq!((a.n2(), a.n2(), a.n2()), (10, 71, 0));
890        assert_eq!(host_command(b"go^m"), b"go\r");
891        assert_eq!(split_on(b"a<>b<>c", b"<>").collect::<Vec<_>>(), [b"a" as &[u8], b"b", b"c"]);
892    }
893
894    #[test]
895    fn draws_lines_bars_and_text_beside_ansi() {
896        // Yellow (14) line, a red bar, then text on the text screen.
897        let t = term(b"!|c0E|L00000A00|S0104|B0A0A0F0F\r\nHello\r\n");
898        assert_eq!((px(&t, 0, 0), px(&t, 10, 0), px(&t, 11, 0)), (14, 14, 0));
899        assert_eq!((px(&t, 12, 12), px(&t, 16, 16)), (4, 0));
900        assert_eq!(Screen::row_text(t.screen.row(0)).trim_end(), "Hello");
901        // `!` not followed by `|` is just text.
902        let t = term(b"!Hi\r\n");
903        assert_eq!(Screen::row_text(t.screen.row(0)).trim_end(), "!Hi");
904    }
905
906    #[test]
907    fn continuation_fill_and_reset() {
908        // A rectangle, continued over a line break, then flood its inside.
909        let t = term(b"!|c0F|R0A0A1414|S01\\\r\n02|F0F0F0F\r\n");
910        assert_eq!((px(&t, 10, 10), px(&t, 15, 15), px(&t, 39, 39), px(&t, 50, 50)), (15, 2, 2, 0));
911        let t = term(b"!|c0F|L00000Z00|*\r\n");
912        assert_eq!(px(&t, 5, 0), 0);
913    }
914
915    #[test]
916    fn buttons_and_mouse_regions() {
917        let t = term(b"!|1B000000000002000F0F0807000000000000|1U0A0A1E140000<>OK<>yes^M\r\n");
918        let rip = t.rip().unwrap();
919        assert_eq!(rip.click(15, 15), Some(b"yes\r".to_vec()));
920        assert_eq!(rip.click(100, 100), None);
921        // The surface is light gray (7), the bevel white above and left.
922        assert_eq!(px(&t, 11, 19), 7);
923        assert_eq!(px(&t, 9, 12), 15);
924        let t = term(b"!|1M00000010101100000say hi\r\n");
925        assert_eq!(t.rip().unwrap().click(5, 5), Some(b"say hi".to_vec()));
926        let t = term(b"!|1M00000010101100000say hi\r\n!|1K\r\n");
927        assert_eq!(t.rip().unwrap().click(5, 5), None);
928    }
929
930    #[test]
931    fn answers_the_rip_query() {
932        let mut t = term(b"\x1b[!");
933        assert_eq!(t.take_replies(), VERSION_REPLY);
934        assert_eq!(ega_rgb(63), 0xFFFFFF);
935        assert_eq!(ega_rgb(20), 0xAA5500);
936    }
937}