use crate::shell::Mode;
use pixel8_runtime::{
assets::Note,
fb::{Framebuffer, HEIGHT, WIDTH},
font,
palette::col,
ui as rui,
};
pub type Icon8 = [u8; 8];
pub fn draw_icon8(fb: &mut Framebuffer, icon: &Icon8, x: i32, y: i32, color: u8) {
for (ry, row) in icon.iter().enumerate() {
for rx in 0..8 {
if row & (0x80 >> rx) != 0 {
fb.pset(x + rx, y + ry as i32, color);
}
}
}
}
pub const ICON_PENCIL: Icon8 = [0x08, 0x1C, 0x3E, 0x7C, 0xB8, 0x90, 0xE0, 0x00];
#[derive(Debug, Clone, Copy)]
pub struct Mouse {
pub x: i32,
pub y: i32,
pub left: bool,
pub right: bool,
pub left_pressed: bool,
pub right_pressed: bool,
}
impl Default for Mouse {
fn default() -> Self {
Self {
x: -16,
y: -16,
left: false,
right: false,
left_pressed: false,
right_pressed: false,
}
}
}
impl Mouse {
pub fn end_frame(&mut self) {
self.left_pressed = false;
self.right_pressed = false;
}
pub fn over(&self, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
self.x >= x0 && self.x <= x1 && self.y >= y0 && self.y <= y1
}
}
const TABS: [(&rui::Icon, Mode); 5] = [
(&rui::ICON_CODE, Mode::Code),
(&rui::ICON_SPRITE, Mode::Sprite),
(&rui::ICON_MAP, Mode::Map),
(&rui::ICON_SFX, Mode::Sfx),
(&rui::ICON_MUSIC, Mode::Music),
];
fn ink_bounds(icon: &rui::Icon) -> (i32, i32) {
let mut lo = 8;
let mut hi = -1;
for &row in icon.iter() {
for c in 0..8 {
if row & (0x80 >> c) != 0 {
lo = lo.min(c);
hi = hi.max(c);
}
}
}
(lo, hi)
}
fn tab_positions() -> [i32; TABS.len()] {
const GAP: i32 = 1;
const RIGHT: i32 = WIDTH - 2;
let n = TABS.len();
let mut xs = [0i32; TABS.len()];
xs[n - 1] = RIGHT - ink_bounds(TABS[n - 1].0).1;
for i in (0..n - 1).rev() {
let hi = ink_bounds(TABS[i].0).1;
let lo_next = ink_bounds(TABS[i + 1].0).0;
xs[i] = xs[i + 1] + lo_next - hi - (GAP + 1);
}
xs
}
fn tab_x(i: usize) -> i32 {
tab_positions()[i]
}
pub fn draw_tab_bar(fb: &mut Framebuffer, active: Mode) {
fb.rectfill(0, 0, WIDTH - 1, 7, col::RED);
for (i, (icon, mode)) in TABS.iter().enumerate() {
let x = tab_x(i);
let color = if *mode == active {
col::PEACH
} else {
col::DARK_PURPLE
};
rui::icon(fb, icon, x, 0, color);
}
}
const FILENAME_X: i32 = 2;
fn filename_limit() -> i32 {
tab_x(0) - 2
}
pub fn code_filename(fb: &mut Framebuffer, name: &str) {
let max = ((filename_limit() - FILENAME_X) / font::GLYPH_W).max(0) as usize;
let shown: String = name.chars().take(max).collect();
fb.print(&shown, FILENAME_X, 1, col::PEACH);
}
pub fn filename_clicked(mouse: &Mouse, name: &str) -> bool {
!name.is_empty()
&& mouse.left_pressed
&& mouse.y < 8
&& mouse.x >= 1
&& mouse.x < filename_limit()
&& mouse.x <= FILENAME_X + font::text_width(name)
}
fn tab_at(x: i32) -> Option<usize> {
(0..TABS.len()).find(|&i| x >= tab_x(i) - 1 && x <= tab_x(i) + 7)
}
pub fn tab_bar_click(mouse: &Mouse) -> Option<usize> {
if !mouse.left_pressed || mouse.y >= 8 {
return None;
}
tab_at(mouse.x)
}
pub fn tab_bar_hover(mouse: &Mouse) -> Option<usize> {
if mouse.y >= 8 {
return None;
}
tab_at(mouse.x)
}
pub fn tab_name(i: usize) -> &'static str {
const NAMES: [&str; TABS.len()] = ["Code", "Sprite", "Map", "SFX", "Music"];
NAMES[i]
}
pub fn status_bar(fb: &mut Framebuffer, text: &str) {
fb.rectfill(0, HEIGHT - 8, WIDTH - 1, HEIGHT - 1, col::RED);
fb.print(text, 2, HEIGHT - 7, col::DARK_PURPLE);
}
pub const STATUS_TTL: u16 = 150;
#[derive(Default)]
pub struct StatusMsg {
text: Option<String>,
ttl: u16,
}
impl StatusMsg {
pub fn set(&mut self, text: String) {
self.text = Some(text);
self.ttl = STATUS_TTL;
}
pub fn tick(&mut self) {
if self.ttl > 0 {
self.ttl -= 1;
if self.ttl == 0 {
self.text = None;
}
}
}
pub fn show(&self, fb: &mut Framebuffer, fallback: &str) {
status_bar(fb, self.text.as_deref().unwrap_or(fallback));
}
#[cfg(test)]
pub fn current(&self) -> Option<&str> {
self.text.as_deref()
}
}
pub fn blit(fb: &mut Framebuffer, x0: i32, y0: i32, rows: &[&str]) {
for (dy, row) in rows.iter().enumerate() {
for (dx, ch) in row.chars().enumerate() {
if ch == '5' {
continue;
}
let c = ch.to_digit(16).unwrap_or(0) as u8;
fb.pset(x0 + dx as i32, y0 + dy as i32, c);
}
}
}
pub fn arrow_l(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
fb.pset(x, y + 2, c);
fb.line(x + 1, y + 1, x + 1, y + 3, c);
fb.line(x + 2, y, x + 2, y + 4, c);
}
pub fn arrow_r(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
fb.line(x, y, x, y + 4, c);
fb.line(x + 1, y + 1, x + 1, y + 3, c);
fb.pset(x + 2, y + 2, c);
}
pub fn mode_buttons(fb: &mut Framebuffer, pitch_active: bool) {
let (bars, grid) = if pitch_active {
(col::PEACH, col::DARK_PURPLE)
} else {
(col::DARK_PURPLE, col::PEACH)
};
for i in 0..4 {
fb.line(5 + i * 2, 1, 5 + i * 2, 6, bars);
}
for r in 0..3 {
for c in 0..4 {
fb.pset(15 + c * 2, 2 + r * 2, grid);
}
}
}
pub fn view_buttons(fb: &mut Framebuffer, fullscreen: bool) {
let (normal, full) = if fullscreen {
(col::DARK_PURPLE, col::PEACH)
} else {
(col::PEACH, col::DARK_PURPLE)
};
fb.rect(5, 1, 11, 6, normal);
fb.line(5, 4, 11, 4, normal);
fb.rect(15, 1, 21, 6, full);
}
pub fn pat_sfx_toggle(fb: &mut Framebuffer, sfx_active: bool) {
let pat_c = if sfx_active {
col::DARK_PURPLE
} else {
col::WHITE
};
let sfx_c = if sfx_active {
col::WHITE
} else {
col::DARK_PURPLE
};
fb.print("Pat", 28, 1, pat_c);
fb.rectfill(43, 3, 56, 4, col::DARK_PURPLE);
let kx = if sfx_active { 51 } else { 43 };
fb.rectfill(kx, 1, kx + 5, 6, col::WHITE);
fb.print("Sfx", 59, 1, sfx_c);
}
pub fn radio(fb: &mut Framebuffer, x: i32, y: i32, on: bool) {
fb.rect(x, y, x + 4, y + 4, col::LIGHT_GREY);
if on {
fb.pset(x + 2, y + 2, col::WHITE);
}
}
pub fn pencil(fb: &mut Framebuffer, x: i32, y: i32) {
fb.line(x + 3, y, x, y + 3, col::LAVENDER);
fb.line(x + 4, y + 1, x + 1, y + 4, col::LAVENDER);
}
const LETTERS: [&str; 12] = ["c", "c", "d", "d", "e", "f", "f", "g", "g", "a", "a", "b"];
const SHARP: [bool; 12] = [
false, true, false, true, false, false, true, false, true, false, true, false,
];
pub fn note_cell(fb: &mut Framebuffer, x: i32, y: i32, note: Note) {
if note.volume == 0 {
for gx in (x + 2..x + 27).step_by(3) {
fb.pset(gx, y + 4, col::DARK_BLUE);
}
return;
}
let k = (note.pitch % 12) as usize;
fb.print(LETTERS[k], x + 2, y, col::WHITE);
if SHARP[k] {
fb.print("#", x + 6, y, col::WHITE);
}
fb.print(&format!("{}", note.pitch / 12), x + 10, y, col::LIGHT_GREY);
let inst_col = if note.instrument().is_some() {
col::GREEN
} else {
col::PINK
};
fb.print(&format!("{}", note.wave_index()), x + 15, y, inst_col);
fb.print(&format!("{}", note.volume), x + 20, y, col::BLUE);
if note.effect == 0 {
fb.print(".", x + 24, y, col::DARK_GREY);
} else {
fb.print(&format!("{}", note.effect), x + 24, y, col::ORANGE);
}
}
pub const FLOW: [&str; 8] = [
"555555555555555555555555555",
"555555c55555555555555555555",
"555555cc5555515515551111555",
"555cccccc555115515551111555",
"555c..cc.551111115551111555",
"555c55c.555.11...5551111555",
"555.55.55555.1555555....555",
"5555555555555.5555555555555",
];
pub const PALETTE: [&str; 6] = [
"888888885666666665666666665666666665666666665666666665666666665",
"888778885666667665666666775667777765666677765667666665667666765",
"887887885666776765666677675667666765666676765676766665767676765",
"878888785677666765667766675667666765666676765766676765777777775",
"788888875766666675776666675777666775777776775766677675676767675",
"888888885666666665666666665666666665666666665666666665676666675",
];
pub const CIRCLE: [&str; 6] = [
"555555555",
"55555dd55",
"5555d55d5",
"5555d55d5",
"55555dd55",
"555555555",
];
pub const WAVEI: [&str; 5] = [
"555555555",
"555d55555",
"55d5d5555",
"5d555d5d5",
"555555d55",
];
pub fn draw_cursor(fb: &mut Framebuffer, mouse: &Mouse) {
rui::cursor(fb, mouse.x, mouse.y);
}
#[cfg(test)]
mod paste_status_tests {
use super::*;
#[test]
fn status_msg_falls_back_then_expires() {
let mut m = StatusMsg::default();
assert_eq!(m.current(), None);
m.set("done".into());
assert_eq!(m.current(), Some("done"));
for _ in 0..STATUS_TTL {
m.tick();
}
assert_eq!(m.current(), None);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pencil_icon_draws_its_lit_pixels() {
let mut fb = Framebuffer::new();
draw_icon8(&mut fb, &ICON_PENCIL, 10, 20, col::WHITE);
assert_eq!(fb.pget(10 + 4, 20), col::WHITE, "row 0 bit 4 lit");
assert_eq!(fb.pget(10, 20), col::BLACK, "row 0 bit 0 unlit");
assert_eq!(fb.pget(10, 26), col::WHITE, "row 6 bit 0 lit");
}
#[test]
fn active_tab_has_no_background_box() {
let mut fb = Framebuffer::new();
draw_tab_bar(&mut fb, Mode::Music);
let x = tab_x(4);
assert_eq!(
fb.pget(x - 1, 3),
col::RED,
"no background box behind the active tab"
);
}
#[test]
fn tab_bar_has_no_title_text() {
let mut fb = Framebuffer::new();
draw_tab_bar(&mut fb, Mode::Code);
for x in 2..18 {
assert_eq!(fb.pget(x, 1), col::RED, "title row must be blank at x={x}");
}
}
#[test]
fn hover_maps_x_to_the_tab_and_its_name() {
let over_first = Mouse {
x: tab_x(0) + 3,
y: 3,
..Default::default()
};
assert_eq!(tab_bar_hover(&over_first), Some(0));
assert_eq!(tab_name(tab_bar_hover(&over_first).unwrap()), "Code");
let last = TABS.len() - 1;
let over_last = Mouse {
x: tab_x(last) + 3,
y: 3,
..Default::default()
};
assert_eq!(tab_name(tab_bar_hover(&over_last).unwrap()), "Music");
let off = Mouse {
x: 1,
y: 3,
..Default::default()
};
assert_eq!(tab_bar_hover(&off), None);
let below = Mouse {
x: tab_x(0) + 3,
y: 8,
..Default::default()
};
assert_eq!(tab_bar_hover(&below), None);
}
#[test]
fn code_filename_draws_and_is_clickable() {
let mut fb = Framebuffer::new();
code_filename(&mut fb, "lib.rs");
let lit = (2..2 + font::text_width("lib.rs"))
.any(|x| (1..7).any(|y| fb.pget(x, y) == col::PEACH));
assert!(lit, "filename should render in peach");
let press = Mouse {
x: 3,
y: 2,
left_pressed: true,
..Default::default()
};
assert!(filename_clicked(&press, "lib.rs"));
assert!(!filename_clicked(&press, ""));
let far = Mouse { x: 120, ..press };
assert!(!filename_clicked(&far, "lib.rs"));
let below = Mouse { y: 9, ..press };
assert!(!filename_clicked(&below, "lib.rs"));
let hover = Mouse {
left_pressed: false,
..press
};
assert!(!filename_clicked(&hover, "lib.rs"));
}
#[test]
fn view_buttons_light_the_active_view() {
let mut fb = Framebuffer::new();
view_buttons(&mut fb, false);
assert_eq!(fb.pget(5, 1), col::PEACH);
assert_eq!(fb.pget(15, 1), col::DARK_PURPLE);
let mut fb2 = Framebuffer::new();
view_buttons(&mut fb2, true);
assert_eq!(fb2.pget(5, 1), col::DARK_PURPLE);
assert_eq!(fb2.pget(15, 1), col::PEACH);
}
#[test]
fn tabs_are_equidistant_by_ink() {
let xs = tab_positions();
let gaps: Vec<i32> = (0..TABS.len() - 1)
.map(|i| {
let this_right = xs[i] + ink_bounds(TABS[i].0).1;
let next_left = xs[i + 1] + ink_bounds(TABS[i + 1].0).0;
next_left - this_right - 1
})
.collect();
assert!(gaps.iter().all(|&g| g == 1), "uneven tab gaps: {gaps:?}");
}
}