1use crate::shell::Mode;
6use pixel8_runtime::{
7 assets::Note,
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 fn blit(fb: &mut Framebuffer, x0: i32, y0: i32, rows: &[&str]) {
247 for (dy, row) in rows.iter().enumerate() {
248 for (dx, ch) in row.chars().enumerate() {
249 if ch == '5' {
250 continue;
251 }
252 let c = ch.to_digit(16).unwrap_or(0) as u8;
253 fb.pset(x0 + dx as i32, y0 + dy as i32, c);
254 }
255 }
256}
257
258pub fn arrow_l(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
260 fb.pset(x, y + 2, c);
261 fb.line(x + 1, y + 1, x + 1, y + 3, c);
262 fb.line(x + 2, y, x + 2, y + 4, c);
263}
264
265pub fn arrow_r(fb: &mut Framebuffer, x: i32, y: i32, c: u8) {
267 fb.line(x, y, x, y + 4, c);
268 fb.line(x + 1, y + 1, x + 1, y + 3, c);
269 fb.pset(x + 2, y + 2, c);
270}
271
272pub fn mode_buttons(fb: &mut Framebuffer, pitch_active: bool) {
275 let (bars, grid) = if pitch_active {
276 (col::PEACH, col::DARK_PURPLE)
277 } else {
278 (col::DARK_PURPLE, col::PEACH)
279 };
280 for i in 0..4 {
281 fb.line(5 + i * 2, 1, 5 + i * 2, 6, bars);
282 }
283 for r in 0..3 {
284 for c in 0..4 {
285 fb.pset(15 + c * 2, 2 + r * 2, grid);
286 }
287 }
288}
289
290pub fn view_buttons(fb: &mut Framebuffer, fullscreen: bool) {
295 let (normal, full) = if fullscreen {
296 (col::DARK_PURPLE, col::PEACH)
297 } else {
298 (col::PEACH, col::DARK_PURPLE)
299 };
300 fb.rect(5, 1, 11, 6, normal);
302 fb.line(5, 4, 11, 4, normal);
303 fb.rect(15, 1, 21, 6, full);
305}
306
307pub fn pat_sfx_toggle(fb: &mut Framebuffer, sfx_active: bool) {
311 let pat_c = if sfx_active {
312 col::DARK_PURPLE
313 } else {
314 col::WHITE
315 };
316 let sfx_c = if sfx_active {
317 col::WHITE
318 } else {
319 col::DARK_PURPLE
320 };
321 fb.print("Pat", 28, 1, pat_c);
322 fb.rectfill(43, 3, 56, 4, col::DARK_PURPLE);
325 let kx = if sfx_active { 51 } else { 43 };
326 fb.rectfill(kx, 1, kx + 5, 6, col::WHITE);
327 fb.print("Sfx", 59, 1, sfx_c);
328}
329
330pub fn radio(fb: &mut Framebuffer, x: i32, y: i32, on: bool) {
332 fb.rect(x, y, x + 4, y + 4, col::LIGHT_GREY);
333 if on {
334 fb.pset(x + 2, y + 2, col::WHITE);
335 }
336}
337
338pub fn pencil(fb: &mut Framebuffer, x: i32, y: i32) {
340 fb.line(x + 3, y, x, y + 3, col::LAVENDER);
341 fb.line(x + 4, y + 1, x + 1, y + 4, col::LAVENDER);
342}
343
344const LETTERS: [&str; 12] = ["c", "c", "d", "d", "e", "f", "f", "g", "g", "a", "a", "b"];
345const SHARP: [bool; 12] = [
346 false, true, false, true, false, false, true, false, true, false, true, false,
347];
348
349pub fn note_cell(fb: &mut Framebuffer, x: i32, y: i32, note: Note) {
354 if note.volume == 0 {
355 for gx in (x + 2..x + 27).step_by(3) {
356 fb.pset(gx, y + 4, col::DARK_BLUE);
357 }
358 return;
359 }
360 let k = (note.pitch % 12) as usize;
361 fb.print(LETTERS[k], x + 2, y, col::WHITE);
362 if SHARP[k] {
363 fb.print("#", x + 6, y, col::WHITE);
364 }
365 fb.print(&format!("{}", note.pitch / 12), x + 10, y, col::LIGHT_GREY);
366 let inst_col = if note.instrument().is_some() {
367 col::GREEN
368 } else {
369 col::PINK
370 };
371 fb.print(&format!("{}", note.wave_index()), x + 15, y, inst_col);
372 fb.print(&format!("{}", note.volume), x + 20, y, col::BLUE);
373 if note.effect == 0 {
374 fb.print(".", x + 24, y, col::DARK_GREY);
375 } else {
376 fb.print(&format!("{}", note.effect), x + 24, y, col::ORANGE);
377 }
378}
379
380pub const FLOW: [&str; 8] = [
382 "555555555555555555555555555",
383 "555555c55555555555555555555",
384 "555555cc5555515515551111555",
385 "555cccccc555115515551111555",
386 "555c..cc.551111115551111555",
387 "555c55c.555.11...5551111555",
388 "555.55.55555.1555555....555",
389 "5555555555555.5555555555555",
390];
391
392pub const PALETTE: [&str; 6] = [
395 "888888885666666665666666665666666665666666665666666665666666665",
396 "888778885666667665666666775667777765666677765667666665667666765",
397 "887887885666776765666677675667666765666676765676766665767676765",
398 "878888785677666765667766675667666765666676765766676765777777775",
399 "788888875766666675776666675777666775777776775766677675676767675",
400 "888888885666666665666666665666666665666666665666666665676666675",
401];
402
403pub const CIRCLE: [&str; 6] = [
405 "555555555",
406 "55555dd55",
407 "5555d55d5",
408 "5555d55d5",
409 "55555dd55",
410 "555555555",
411];
412
413pub const WAVEI: [&str; 5] = [
415 "555555555",
416 "555d55555",
417 "55d5d5555",
418 "5d555d5d5",
419 "555555d55",
420];
421
422pub fn draw_cursor(fb: &mut Framebuffer, mouse: &Mouse) {
424 rui::cursor(fb, mouse.x, mouse.y);
425}
426
427#[cfg(test)]
428mod paste_status_tests {
429 use super::*;
430
431 #[test]
432 fn status_msg_falls_back_then_expires() {
433 let mut m = StatusMsg::default();
434 assert_eq!(m.current(), None);
435 m.set("done".into());
436 assert_eq!(m.current(), Some("done"));
437 for _ in 0..STATUS_TTL {
438 m.tick();
439 }
440 assert_eq!(m.current(), None);
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn pencil_icon_draws_its_lit_pixels() {
450 let mut fb = Framebuffer::new();
451 draw_icon8(&mut fb, &ICON_PENCIL, 10, 20, col::WHITE);
452 assert_eq!(fb.pget(10 + 4, 20), col::WHITE, "row 0 bit 4 lit");
454 assert_eq!(fb.pget(10, 20), col::BLACK, "row 0 bit 0 unlit");
455 assert_eq!(fb.pget(10, 26), col::WHITE, "row 6 bit 0 lit");
457 }
458
459 #[test]
460 fn active_tab_has_no_background_box() {
461 let mut fb = Framebuffer::new();
462 draw_tab_bar(&mut fb, Mode::Music);
463 let x = tab_x(4);
466 assert_eq!(
467 fb.pget(x - 1, 3),
468 col::RED,
469 "no background box behind the active tab"
470 );
471 }
472
473 #[test]
474 fn tab_bar_has_no_title_text() {
475 let mut fb = Framebuffer::new();
477 draw_tab_bar(&mut fb, Mode::Code);
478 for x in 2..18 {
479 assert_eq!(fb.pget(x, 1), col::RED, "title row must be blank at x={x}");
480 }
481 }
482
483 #[test]
484 fn hover_maps_x_to_the_tab_and_its_name() {
485 let over_first = Mouse {
487 x: tab_x(0) + 3,
488 y: 3,
489 ..Default::default()
490 };
491 assert_eq!(tab_bar_hover(&over_first), Some(0));
492 assert_eq!(tab_name(tab_bar_hover(&over_first).unwrap()), "Code");
493 let last = TABS.len() - 1;
494 let over_last = Mouse {
495 x: tab_x(last) + 3,
496 y: 3,
497 ..Default::default()
498 };
499 assert_eq!(tab_name(tab_bar_hover(&over_last).unwrap()), "Music");
500 let off = Mouse {
502 x: 1,
503 y: 3,
504 ..Default::default()
505 };
506 assert_eq!(tab_bar_hover(&off), None);
507 let below = Mouse {
508 x: tab_x(0) + 3,
509 y: 8,
510 ..Default::default()
511 };
512 assert_eq!(tab_bar_hover(&below), None);
513 }
514
515 #[test]
516 fn code_filename_draws_and_is_clickable() {
517 let mut fb = Framebuffer::new();
518 code_filename(&mut fb, "lib.rs");
519 let lit = (2..2 + font::text_width("lib.rs"))
521 .any(|x| (1..7).any(|y| fb.pget(x, y) == col::PEACH));
522 assert!(lit, "filename should render in peach");
523
524 let press = Mouse {
525 x: 3,
526 y: 2,
527 left_pressed: true,
528 ..Default::default()
529 };
530 assert!(filename_clicked(&press, "lib.rs"));
531 assert!(!filename_clicked(&press, ""));
533 let far = Mouse { x: 120, ..press };
535 assert!(!filename_clicked(&far, "lib.rs"));
536 let below = Mouse { y: 9, ..press };
537 assert!(!filename_clicked(&below, "lib.rs"));
538 let hover = Mouse {
539 left_pressed: false,
540 ..press
541 };
542 assert!(!filename_clicked(&hover, "lib.rs"));
543 }
544
545 #[test]
546 fn view_buttons_light_the_active_view() {
547 let mut fb = Framebuffer::new();
549 view_buttons(&mut fb, false);
550 assert_eq!(fb.pget(5, 1), col::PEACH);
551 assert_eq!(fb.pget(15, 1), col::DARK_PURPLE);
552 let mut fb2 = Framebuffer::new();
554 view_buttons(&mut fb2, true);
555 assert_eq!(fb2.pget(5, 1), col::DARK_PURPLE);
556 assert_eq!(fb2.pget(15, 1), col::PEACH);
557 }
558
559 #[test]
560 fn tabs_are_equidistant_by_ink() {
561 let xs = tab_positions();
564 let gaps: Vec<i32> = (0..TABS.len() - 1)
565 .map(|i| {
566 let this_right = xs[i] + ink_bounds(TABS[i].0).1;
567 let next_left = xs[i + 1] + ink_bounds(TABS[i + 1].0).0;
568 next_left - this_right - 1
569 })
570 .collect();
571 assert!(gaps.iter().all(|&g| g == 1), "uneven tab gaps: {gaps:?}");
572 }
573}