1use crate::shell::Mode;
6use pixel8_runtime::{
7 assets::{Note, SpriteSheet, SPRITES_PER_ROW, SPRITE_COUNT},
8 fb::{Framebuffer, HEIGHT, WIDTH},
9 font,
10 palette::col,
11 ui as rui,
12};
13
14pub type Icon8 = [u8; 8];
16
17pub fn draw_icon8(fb: &mut Framebuffer, icon: &Icon8, x: i32, y: i32, color: u8) {
19 for (ry, row) in icon.iter().enumerate() {
20 for rx in 0..8 {
21 if row & (0x80 >> rx) != 0 {
22 fb.pset(x + rx, y + ry as i32, color);
23 }
24 }
25 }
26}
27
28pub const ICON_PENCIL: Icon8 = [0x08, 0x1C, 0x3E, 0x7C, 0xB8, 0x90, 0xE0, 0x00];
31
32#[derive(Debug, Clone, Copy)]
34pub struct Mouse {
35 pub x: i32,
36 pub y: i32,
37 pub left: bool,
38 pub right: bool,
39 pub left_pressed: bool,
41 pub right_pressed: bool,
42}
43
44impl Default for Mouse {
45 fn default() -> Self {
46 Self {
48 x: -16,
49 y: -16,
50 left: false,
51 right: false,
52 left_pressed: false,
53 right_pressed: false,
54 }
55 }
56}
57
58impl Mouse {
59 pub fn end_frame(&mut self) {
61 self.left_pressed = false;
62 self.right_pressed = false;
63 }
64
65 pub fn over(&self, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
66 self.x >= x0 && self.x <= x1 && self.y >= y0 && self.y <= y1
67 }
68}
69
70const TABS: [(&rui::Icon, Mode); 5] = [
71 (&rui::ICON_CODE, Mode::Code),
72 (&rui::ICON_SPRITE, Mode::Sprite),
73 (&rui::ICON_MAP, Mode::Map),
74 (&rui::ICON_SFX, Mode::Sfx),
75 (&rui::ICON_MUSIC, Mode::Music),
76];
77
78fn ink_bounds(icon: &rui::Icon) -> (i32, i32) {
80 let mut lo = 8;
81 let mut hi = -1;
82 for &row in icon.iter() {
83 for c in 0..8 {
84 if row & (0x80 >> c) != 0 {
85 lo = lo.min(c);
86 hi = hi.max(c);
87 }
88 }
89 }
90 (lo, hi)
91}
92
93fn tab_positions() -> [i32; TABS.len()] {
97 const GAP: i32 = 1;
99 const RIGHT: i32 = WIDTH - 2;
101 let n = TABS.len();
102 let mut xs = [0i32; TABS.len()];
103 xs[n - 1] = RIGHT - ink_bounds(TABS[n - 1].0).1;
104 for i in (0..n - 1).rev() {
105 let hi = ink_bounds(TABS[i].0).1;
106 let lo_next = ink_bounds(TABS[i + 1].0).0;
107 xs[i] = xs[i + 1] + lo_next - hi - (GAP + 1);
108 }
109 xs
110}
111
112fn tab_x(i: usize) -> i32 {
113 tab_positions()[i]
114}
115
116pub fn draw_tab_bar(fb: &mut Framebuffer, active: Mode) {
120 fb.rectfill(0, 0, WIDTH - 1, 7, col::RED);
121 for (i, (icon, mode)) in TABS.iter().enumerate() {
124 let x = tab_x(i);
125 let color = if *mode == active {
126 col::PEACH
127 } else {
128 col::DARK_PURPLE
129 };
130 rui::icon(fb, icon, x, 0, color);
131 }
132}
133
134const FILENAME_X: i32 = 2;
136
137fn filename_limit() -> i32 {
141 tab_x(0) - 2
142}
143
144pub fn code_filename(fb: &mut Framebuffer, name: &str) {
148 let max = ((filename_limit() - FILENAME_X) / font::GLYPH_W).max(0) as usize;
149 let shown: String = name.chars().take(max).collect();
150 fb.print(&shown, FILENAME_X, 1, col::PEACH);
151}
152
153pub fn filename_clicked(mouse: &Mouse, name: &str) -> bool {
157 !name.is_empty()
158 && mouse.left_pressed
159 && mouse.y < 8
160 && mouse.x >= 1
161 && mouse.x < filename_limit()
162 && mouse.x <= FILENAME_X + font::text_width(name)
163}
164
165fn tab_at(x: i32) -> Option<usize> {
167 (0..TABS.len()).find(|&i| x >= tab_x(i) - 1 && x <= tab_x(i) + 7)
168}
169
170pub fn tab_bar_click(mouse: &Mouse) -> Option<usize> {
172 if !mouse.left_pressed || mouse.y >= 8 {
173 return None;
174 }
175 tab_at(mouse.x)
176}
177
178pub fn tab_bar_hover(mouse: &Mouse) -> Option<usize> {
180 if mouse.y >= 8 {
181 return None;
182 }
183 tab_at(mouse.x)
184}
185
186pub fn tab_name(i: usize) -> &'static str {
188 const NAMES: [&str; TABS.len()] = ["Code", "Sprite", "Map", "SFX", "Music"];
189 NAMES[i]
190}
191
192pub fn status_bar(fb: &mut Framebuffer, text: &str) {
194 fb.rectfill(0, HEIGHT - 8, WIDTH - 1, HEIGHT - 1, col::RED);
195 fb.print(text, 2, HEIGHT - 7, col::DARK_PURPLE);
196}
197
198pub const STATUS_TTL: u16 = 150;
200
201#[derive(Default)]
204pub struct StatusMsg {
205 text: Option<String>,
206 ttl: u16,
207}
208
209impl StatusMsg {
210 pub fn set(&mut self, text: String) {
212 self.text = Some(text);
213 self.ttl = STATUS_TTL;
214 }
215
216 pub fn tick(&mut self) {
218 if self.ttl > 0 {
219 self.ttl -= 1;
220 if self.ttl == 0 {
221 self.text = None;
222 }
223 }
224 }
225
226 pub fn show(&self, fb: &mut Framebuffer, fallback: &str) {
228 status_bar(fb, self.text.as_deref().unwrap_or(fallback));
229 }
230
231 #[cfg(test)]
233 pub fn current(&self) -> Option<&str> {
234 self.text.as_deref()
235 }
236}
237
238pub const STRIP_H: i32 = 32;
244
245pub fn sprite_sample(sheet: &SpriteSheet, ox: i32, oy: i32, n: i32, px: i32, py: i32) -> u8 {
250 let step = 8 / n;
251 sheet.get(ox + px * step + step / 2, oy + py * step + step / 2)
252}
253
254const SHEET_COLS: i32 = SPRITES_PER_ROW as i32;
255const SHEET_ROWS: i32 = (SPRITE_COUNT / SPRITES_PER_ROW) as i32;
256
257#[derive(Debug, Clone, Copy)]
265pub struct Strip {
266 pub cell: i32,
268 pub rows: i32,
270 pub x0: i32,
272 pub row0: i32,
274}
275
276impl Strip {
277 pub fn new(size: i32, row: i32, page: u32) -> Self {
280 let follows = Self::follows_block(size);
281 let (cell, rows) = if follows { (4, 8) } else { (8, 4) };
282 let row0 = if follows { row } else { page as i32 * rows };
283 Self {
284 cell,
285 rows,
286 x0: (WIDTH - SHEET_COLS * cell) / 2,
287 row0: row0.clamp(0, SHEET_ROWS - rows),
288 }
289 }
290
291 pub fn follows_block(size: i32) -> bool {
294 size * 8 > STRIP_H
295 }
296
297 pub fn cell_at(&self, top: i32, x: i32, y: i32) -> Option<(i32, i32)> {
300 let (cx, cy) = ((x - self.x0) / self.cell, (y - top) / self.cell);
301 let inside = x >= self.x0 && cx < SHEET_COLS && y >= top && cy < self.rows;
302 inside.then_some((cx, self.row0 + cy))
303 }
304
305 pub fn draw(&self, fb: &mut Framebuffer, sprites: &SpriteSheet, top: i32) {
308 fb.rectfill(0, top, WIDTH - 1, top + STRIP_H - 1, col::DARK_GREY);
309 for cy in 0..self.rows {
310 for cx in 0..SHEET_COLS {
311 let (sx, sy) = (cx * 8, (self.row0 + cy) * 8);
312 for py in 0..self.cell {
313 for px in 0..self.cell {
314 fb.pset(
315 self.x0 + cx * self.cell + px,
316 top + cy * self.cell + py,
317 sprite_sample(sprites, sx, sy, self.cell, px, py),
318 );
319 }
320 }
321 }
322 }
323 }
324
325 pub fn draw_block(&self, fb: &mut Framebuffer, top: i32, col: i32, row: i32, size: i32) {
328 let y0 = (row - self.row0).max(0);
329 let y1 = (row + size - self.row0).min(self.rows);
330 if y1 <= y0 {
331 return;
332 }
333 let x = self.x0 + col * self.cell;
334 let y = top + y0 * self.cell;
335 fb.rect(
336 x,
337 y,
338 x + size * self.cell - 1,
339 y + (y1 - y0) * self.cell - 1,
340 col::WHITE,
341 );
342 }
343}
344
345pub fn blit(fb: &mut Framebuffer, x0: i32, y0: i32, rows: &[&str]) {
354 for (dy, row) in rows.iter().enumerate() {
355 for (dx, ch) in row.chars().enumerate() {
356 if ch == '5' {
357 continue;
358 }
359 let c = ch.to_digit(16).unwrap_or(0) as u8;
360 fb.pset(x0 + dx as i32, y0 + dy as i32, c);
361 }
362 }
363}
364
365pub fn arrow_l(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
367 fb.pset(x, y + 2, c);
368 fb.line(x + 1, y + 1, x + 1, y + 3, c);
369 fb.line(x + 2, y, x + 2, y + 4, c);
370}
371
372pub fn arrow_r(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
374 fb.line(x, y, x, y + 4, c);
375 fb.line(x + 1, y + 1, x + 1, y + 3, c);
376 fb.pset(x + 2, y + 2, c);
377}
378
379pub fn mode_buttons(fb: &mut Framebuffer, pitch_active: bool) {
382 let (bars, grid) = if pitch_active {
383 (col::PEACH, col::DARK_PURPLE)
384 } else {
385 (col::DARK_PURPLE, col::PEACH)
386 };
387 for i in 0..4 {
388 fb.line(5 + i * 2, 1, 5 + i * 2, 6, bars);
389 }
390 for r in 0..3 {
391 for c in 0..4 {
392 fb.pset(15 + c * 2, 2 + r * 2, grid);
393 }
394 }
395}
396
397pub fn view_buttons(fb: &mut Framebuffer, fullscreen: bool) {
402 let (normal, full) = if fullscreen {
403 (col::DARK_PURPLE, col::PEACH)
404 } else {
405 (col::PEACH, col::DARK_PURPLE)
406 };
407 fb.rect(5, 1, 11, 6, normal);
409 fb.line(5, 4, 11, 4, normal);
410 fb.rect(15, 1, 21, 6, full);
412}
413
414pub fn pat_sfx_toggle(fb: &mut Framebuffer, sfx_active: bool) {
418 let pat_c = if sfx_active {
419 col::DARK_PURPLE
420 } else {
421 col::WHITE
422 };
423 let sfx_c = if sfx_active {
424 col::WHITE
425 } else {
426 col::DARK_PURPLE
427 };
428 fb.print("Pat", 28, 1, pat_c);
429 fb.rectfill(43, 3, 56, 4, col::DARK_PURPLE);
432 let kx = if sfx_active { 51 } else { 43 };
433 fb.rectfill(kx, 1, kx + 5, 6, col::WHITE);
434 fb.print("Sfx", 59, 1, sfx_c);
435}
436
437pub fn radio(fb: &mut Framebuffer, x: i32, y: i32, on: bool) {
439 fb.rect(x, y, x + 4, y + 4, col::LIGHT_GREY);
440 if on {
441 fb.pset(x + 2, y + 2, col::WHITE);
442 }
443}
444
445pub fn pencil(fb: &mut Framebuffer, x: i32, y: i32) {
447 fb.line(x + 3, y, x, y + 3, col::LAVENDER);
448 fb.line(x + 4, y + 1, x + 1, y + 4, col::LAVENDER);
449}
450
451const LETTERS: [&str; 12] = ["c", "c", "d", "d", "e", "f", "f", "g", "g", "a", "a", "b"];
452const SHARP: [bool; 12] = [
453 false, true, false, true, false, false, true, false, true, false, true, false,
454];
455
456pub fn note_cell(fb: &mut Framebuffer, x: i32, y: i32, note: Note) {
461 if note.volume == 0 {
462 for gx in (x + 2..x + 27).step_by(3) {
463 fb.pset(gx, y + 4, col::DARK_BLUE);
464 }
465 return;
466 }
467 let k = (note.pitch % 12) as usize;
468 fb.print(LETTERS[k], x + 2, y, col::WHITE);
469 if SHARP[k] {
470 fb.print("#", x + 6, y, col::WHITE);
471 }
472 fb.print(&format!("{}", note.pitch / 12), x + 10, y, col::LIGHT_GREY);
473 let inst_col = if note.instrument().is_some() {
474 col::GREEN
475 } else {
476 col::PINK
477 };
478 fb.print(&format!("{}", note.wave_index()), x + 15, y, inst_col);
479 fb.print(&format!("{}", note.volume), x + 20, y, col::BLUE);
480 if note.effect == 0 {
481 fb.print(".", x + 24, y, col::DARK_GREY);
482 } else {
483 fb.print(&format!("{}", note.effect), x + 24, y, col::ORANGE);
484 }
485}
486
487pub const FLOW: [&str; 8] = [
489 "555555555555555555555555555",
490 "555555c55555555555555555555",
491 "555555cc5555515515551111555",
492 "555cccccc555115515551111555",
493 "555c..cc.551111115551111555",
494 "555c55c.555.11...5551111555",
495 "555.55.55555.1555555....555",
496 "5555555555555.5555555555555",
497];
498
499pub const PALETTE: [&str; 6] = [
502 "888888885666666665666666665666666665666666665666666665666666665",
503 "888778885666667665666666775667777765666677765667666665667666765",
504 "887887885666776765666677675667666765666676765676766665767676765",
505 "878888785677666765667766675667666765666676765766676765777777775",
506 "788888875766666675776666675777666775777776775766677675676767675",
507 "888888885666666665666666665666666665666666665666666665676666675",
508];
509
510pub const CIRCLE: [&str; 6] = [
512 "555555555",
513 "55555dd55",
514 "5555d55d5",
515 "5555d55d5",
516 "55555dd55",
517 "555555555",
518];
519
520pub const WAVEI: [&str; 5] = [
522 "555555555",
523 "555d55555",
524 "55d5d5555",
525 "5d555d5d5",
526 "555555d55",
527];
528
529pub fn draw_cursor(fb: &mut Framebuffer, mouse: &Mouse) {
531 rui::cursor(fb, mouse.x, mouse.y);
532}
533
534#[cfg(test)]
535mod paste_status_tests {
536 use super::*;
537
538 #[test]
539 fn status_msg_falls_back_then_expires() {
540 let mut m = StatusMsg::default();
541 assert_eq!(m.current(), None);
542 m.set("done".into());
543 assert_eq!(m.current(), Some("done"));
544 for _ in 0..STATUS_TTL {
545 m.tick();
546 }
547 assert_eq!(m.current(), None);
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn pencil_icon_draws_its_lit_pixels() {
557 let mut fb = Framebuffer::new();
558 draw_icon8(&mut fb, &ICON_PENCIL, 10, 20, col::WHITE);
559 assert_eq!(fb.pget(10 + 4, 20), col::WHITE, "row 0 bit 4 lit");
561 assert_eq!(fb.pget(10, 20), col::BLACK, "row 0 bit 0 unlit");
562 assert_eq!(fb.pget(10, 26), col::WHITE, "row 6 bit 0 lit");
564 }
565
566 #[test]
567 fn active_tab_has_no_background_box() {
568 let mut fb = Framebuffer::new();
569 draw_tab_bar(&mut fb, Mode::Music);
570 let x = tab_x(4);
573 assert_eq!(
574 fb.pget(x - 1, 3),
575 col::RED,
576 "no background box behind the active tab"
577 );
578 }
579
580 #[test]
581 fn tab_bar_has_no_title_text() {
582 let mut fb = Framebuffer::new();
584 draw_tab_bar(&mut fb, Mode::Code);
585 for x in 2..18 {
586 assert_eq!(fb.pget(x, 1), col::RED, "title row must be blank at x={x}");
587 }
588 }
589
590 #[test]
591 fn hover_maps_x_to_the_tab_and_its_name() {
592 let over_first = Mouse {
594 x: tab_x(0) + 3,
595 y: 3,
596 ..Default::default()
597 };
598 assert_eq!(tab_bar_hover(&over_first), Some(0));
599 assert_eq!(tab_name(tab_bar_hover(&over_first).unwrap()), "Code");
600 let last = TABS.len() - 1;
601 let over_last = Mouse {
602 x: tab_x(last) + 3,
603 y: 3,
604 ..Default::default()
605 };
606 assert_eq!(tab_name(tab_bar_hover(&over_last).unwrap()), "Music");
607 let off = Mouse {
609 x: 1,
610 y: 3,
611 ..Default::default()
612 };
613 assert_eq!(tab_bar_hover(&off), None);
614 let below = Mouse {
615 x: tab_x(0) + 3,
616 y: 8,
617 ..Default::default()
618 };
619 assert_eq!(tab_bar_hover(&below), None);
620 }
621
622 #[test]
623 fn code_filename_draws_and_is_clickable() {
624 let mut fb = Framebuffer::new();
625 code_filename(&mut fb, "lib.rs");
626 let lit = (2..2 + font::text_width("lib.rs"))
628 .any(|x| (1..7).any(|y| fb.pget(x, y) == col::PEACH));
629 assert!(lit, "filename should render in peach");
630
631 let press = Mouse {
632 x: 3,
633 y: 2,
634 left_pressed: true,
635 ..Default::default()
636 };
637 assert!(filename_clicked(&press, "lib.rs"));
638 assert!(!filename_clicked(&press, ""));
640 let far = Mouse { x: 120, ..press };
642 assert!(!filename_clicked(&far, "lib.rs"));
643 let below = Mouse { y: 9, ..press };
644 assert!(!filename_clicked(&below, "lib.rs"));
645 let hover = Mouse {
646 left_pressed: false,
647 ..press
648 };
649 assert!(!filename_clicked(&hover, "lib.rs"));
650 }
651
652 #[test]
653 fn view_buttons_light_the_active_view() {
654 let mut fb = Framebuffer::new();
656 view_buttons(&mut fb, false);
657 assert_eq!(fb.pget(5, 1), col::PEACH);
658 assert_eq!(fb.pget(15, 1), col::DARK_PURPLE);
659 let mut fb2 = Framebuffer::new();
661 view_buttons(&mut fb2, true);
662 assert_eq!(fb2.pget(5, 1), col::DARK_PURPLE);
663 assert_eq!(fb2.pget(15, 1), col::PEACH);
664 }
665
666 #[test]
667 fn tabs_are_equidistant_by_ink() {
668 let xs = tab_positions();
671 let gaps: Vec<i32> = (0..TABS.len() - 1)
672 .map(|i| {
673 let this_right = xs[i] + ink_bounds(TABS[i].0).1;
674 let next_left = xs[i + 1] + ink_bounds(TABS[i + 1].0).0;
675 next_left - this_right - 1
676 })
677 .collect();
678 assert!(gaps.iter().all(|&g| g == 1), "uneven tab gaps: {gaps:?}");
679 }
680}