Skip to main content

pixel8_runtime/
ui.rs

1//! Fantasy-console UI primitives.
2//!
3//! Everything the shell and editors draw — panels, tab icons, the mouse
4//! cursor, selections — goes through these helpers into the same 128x128
5//! framebuffer carts use. There are deliberately no native-looking
6//! widgets here: this is console chrome, not a GUI toolkit.
7
8use crate::{fb::Framebuffer, palette::col};
9
10/// Filled panel with a 1px border, inclusive corners.
11pub fn panel(fb: &mut Framebuffer, x0: i32, y0: i32, x1: i32, y1: i32, bg: u8, border: u8) {
12    fb.rectfill(x0, y0, x1, y1, bg);
13    fb.rect(x0, y0, x1, y1, border);
14}
15
16/// Text with a 1px drop shadow, for headers on busy backgrounds.
17pub fn shadow_text(fb: &mut Framebuffer, s: &str, x: i32, y: i32, color: u8, shadow: u8) {
18    fb.print(s, x + 1, y + 1, shadow);
19    fb.print(s, x, y, color);
20}
21
22/// Invert-style selection: repaint a rectangle's pixels with swapped
23/// foreground/background, used for text selections and highlights.
24pub fn selection(fb: &mut Framebuffer, x0: i32, y0: i32, x1: i32, y1: i32, fg: u8, bg: u8) {
25    for y in y0..=y1 {
26        for x in x0..=x1 {
27            let c = fb.pget(x, y);
28            let n = if c == bg {
29                fg
30            } else if c == fg {
31                bg
32            } else {
33                c
34            };
35            fb.pset(x, y, n);
36        }
37    }
38}
39
40/// 8x8 single-color icons for the editor tab bar, one bit per pixel.
41/// Order matches `TAB_ICONS`' documentation: code, sprite, map, sfx, music.
42pub type Icon = [u8; 8];
43
44/// `R`: the code editor.
45pub const ICON_CODE: Icon = [0x00, 0x7C, 0x42, 0x44, 0x78, 0x44, 0x42, 0x00];
46/// A crab: the sprite editor.
47pub const ICON_SPRITE: Icon = [0x00, 0xA5, 0x42, 0x7E, 0xDB, 0x7E, 0x24, 0x00];
48/// Tile grid: the map editor.
49pub const ICON_MAP: Icon = [0x00, 0xFF, 0x91, 0xFF, 0x91, 0x91, 0xFF, 0x00];
50/// Speaker: the SFX editor.
51pub const ICON_SFX: Icon = [0x00, 0x06, 0x0E, 0x7E, 0x7E, 0x0E, 0x06, 0x00];
52/// Note: the music editor.
53pub const ICON_MUSIC: Icon = [0x00, 0x3C, 0x24, 0x20, 0x20, 0xE0, 0xE0, 0x00];
54
55/// Draw an icon in one color (bits set = pixels drawn).
56pub fn icon(fb: &mut Framebuffer, icon: &Icon, x: i32, y: i32, color: u8) {
57    for (ry, row) in icon.iter().enumerate() {
58        for rx in 0..8 {
59            if row & (0x80 >> rx) != 0 {
60                fb.pset(x + rx, y + ry as i32, color);
61            }
62        }
63    }
64}
65
66/// Mouse cursor: white arrow with a black outline, hotspot at (0, 0).
67/// First layer is the white fill, second the black outline.
68const CURSOR_FILL: Icon = [
69    0b00000000, 0b01000000, 0b01100000, 0b01110000, 0b01111000, 0b01100000, 0b00100000, 0b00000000,
70];
71const CURSOR_OUTLINE: Icon = [
72    0b11000000, 0b10100000, 0b10010000, 0b10001000, 0b10000100, 0b10011100, 0b11010000, 0b00110000,
73];
74
75/// The console's friendly runtime-error screen, shared by every
76/// frontend (desktop, web, handheld) so a crashed cart looks the same
77/// everywhere.
78pub fn error_screen(message: &str) -> Framebuffer {
79    use crate::fb::{HEIGHT, WIDTH};
80    let mut fb = Framebuffer::new();
81    fb.cls(col::BLACK);
82    fb.rectfill(0, 0, WIDTH - 1, 7, col::RED);
83    fb.print("Pixel8", 2, 1, col::WHITE);
84    fb.print("** Runtime error **", 2, 14, col::RED);
85    let mut y = 24;
86    for line in message.lines().take(12) {
87        let mut rest = line;
88        while !rest.is_empty() && y < HEIGHT - 8 {
89            let take = rest
90                .char_indices()
91                .nth(31)
92                .map(|(i, _)| i)
93                .unwrap_or(rest.len());
94            fb.print(&rest[..take], 2, y, col::ORANGE);
95            rest = &rest[take..];
96            y += 6;
97        }
98    }
99    fb
100}
101
102/// Draw the mouse cursor at a framebuffer position.
103pub fn cursor(fb: &mut Framebuffer, x: i32, y: i32) {
104    for (icon, color) in [(&CURSOR_OUTLINE, col::BLACK), (&CURSOR_FILL, col::WHITE)] {
105        for (ry, row) in icon.iter().enumerate() {
106            for rx in 0..8 {
107                if row & (0x80 >> rx) != 0 {
108                    fb.pset(x + rx, y + ry as i32, color);
109                }
110            }
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn tab_icons_share_a_uniform_envelope() {
121        for (name, icon) in [
122            ("code", &ICON_CODE),
123            ("sprite", &ICON_SPRITE),
124            ("map", &ICON_MAP),
125            ("sfx", &ICON_SFX),
126            ("music", &ICON_MUSIC),
127        ] {
128            assert_eq!(
129                icon[0], 0,
130                "{name} icon: row 0 must be blank for even top spacing"
131            );
132            assert_eq!(
133                icon[7], 0,
134                "{name} icon: row 7 must be blank for even bottom spacing"
135            );
136        }
137    }
138}