diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index 97dd521e..e1554d0d 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -51,7 +51,7 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let advance_width = letter_data.advance_width.ceil() as u8; quote!( - agb::display::FontLetter::new( + display::FontLetter::new( #width, #height, #data_raw, @@ -63,6 +63,6 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { }); quote![ - agb::display::Font::new(&[#(#font),*], #line_height, #ascent) + display::Font::new(&[#(#font),*], #line_height, #ascent) ] } diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index e7885090..c82cbd09 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -311,6 +311,13 @@ fn palete_tile_data( (palette_data, tile_data, assignments) } +fn flatten_group(expr: &Expr) -> &Expr { + match expr { + Expr::Group(group) => &group.expr, + _ => expr, + } +} + #[proc_macro] pub fn include_font(input: TokenStream) -> TokenStream { let parser = Punctuated::::parse_separated_nonempty; @@ -324,7 +331,7 @@ pub fn include_font(input: TokenStream) -> TokenStream { panic!("Include_font requires 2 arguments, got {}", all_args.len()); } - let filename = match &all_args[0] { + let filename = match flatten_group(&all_args[0]) { Expr::Lit(ExprLit { lit: Lit::Str(str_lit), .. @@ -332,7 +339,7 @@ pub fn include_font(input: TokenStream) -> TokenStream { _ => panic!("Expected literal string as first argument to include_font"), }; - let font_size = match &all_args[1] { + let font_size = match flatten_group(&all_args[1]) { Expr::Lit(ExprLit { lit: Lit::Float(value), .. diff --git a/agb/examples/font/font-test-output.png b/agb/examples/font/font-test-output.png new file mode 100644 index 00000000..b07074d5 Binary files /dev/null and b/agb/examples/font/font-test-output.png differ diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index eccde3e3..62bac78a 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -201,3 +201,48 @@ impl<'a> Drop for TextRenderer<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + const FONT: Font = crate::include_font!("examples/font/yoster.ttf", 12); + + #[test_case] + fn font_display(gba: &mut crate::Gba) { + let (gfx, mut vram) = gba.display.video.tiled0(); + + let mut bg = gfx.background(crate::display::Priority::P0); + + vram.set_background_palette_raw(&[ + 0x0000, 0x0ff0, 0x00ff, 0xf00f, 0xf0f0, 0x0f0f, 0xaaaa, 0x5555, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + ]); + + let background_tile = vram.new_dynamic_tile().fill_with(0); + + for y in 0..20u16 { + for x in 0..30u16 { + bg.set_tile( + &mut vram, + (x, y).into(), + &background_tile.tile_set(), + TileSetting::from_raw(background_tile.tile_index()), + ); + } + } + + vram.remove_dynamic_tile(background_tile); + + let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram); + + writeln!(&mut writer, "Hello, World!").unwrap(); + writeln!(&mut writer, "This is a font rendering example").unwrap(); + + writer.commit(); + + bg.commit(); + bg.show(); + + crate::test_runner::assert_image_output("examples/font/font-test-output.png"); + } +} diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index c57b2809..7336e916 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -961,22 +961,26 @@ mod tests { let object = gba.display.object.get(); - let mut objects: Vec<_> = alloc::vec![ - object.object(object.sprite(BOSS.sprite(0))), - object.object(object.sprite(EMU.sprite(0))), - ] - .into_iter() - .map(Some) - .collect(); + { + let mut objects: Vec<_> = alloc::vec![ + object.object(object.sprite(BOSS.sprite(0))), + object.object(object.sprite(EMU.sprite(0))), + ] + .into_iter() + .map(Some) + .collect(); - object.commit(); + object.commit(); - let x = objects[0].as_mut().unwrap(); - x.set_hflip(true); - x.set_vflip(true); - x.set_position((1, 1).into()); - x.set_z(100); - x.set_sprite(object.sprite(BOSS.sprite(2))); + let x = objects[0].as_mut().unwrap(); + x.set_hflip(true); + x.set_vflip(true); + x.set_position((1, 1).into()); + x.set_z(100); + x.set_sprite(object.sprite(BOSS.sprite(2))); + + object.commit(); + } object.commit(); } diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 2ea42ddb..9eb473cc 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -112,9 +112,19 @@ /// ``` pub use agb_image_converter::include_gfx; +#[doc(hidden)] pub use agb_image_converter::include_aseprite_inner; -pub use agb_image_converter::include_font; +#[doc(hidden)] +pub use agb_image_converter::include_font as include_font_inner; + +#[macro_export] +macro_rules! include_font { + ($font_path: literal, $font_size: literal) => {{ + use $crate::display; + $crate::include_font_inner!($font_path, $font_size) + }}; +} /// This macro declares the entry point to your game written using `agb`. /// diff --git a/mgba-test-runner/src/main.rs b/mgba-test-runner/src/main.rs index 97ead86a..e50ad5dc 100644 --- a/mgba-test-runner/src/main.rs +++ b/mgba-test-runner/src/main.rs @@ -3,6 +3,7 @@ mod runner; use anyhow::{anyhow, Error}; use image::io::Reader; +use image::GenericImage; use io::Write; use regex::Regex; use runner::VideoBuffer; @@ -135,15 +136,14 @@ fn rgba_to_gba_to_rgba(c: [u8; 4]) -> [u8; 4] { fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), Error> { let expected_image = Reader::open(image_path)?.decode()?; - let expected = expected_image - .as_rgba8() - .ok_or(anyhow!("cannot convert to rgba8"))?; + let expected = expected_image.to_rgba8(); let (buf_dim_x, buf_dim_y) = video_buffer.get_size(); let (exp_dim_x, exp_dim_y) = expected.dimensions(); if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) { return Err(anyhow!("image sizes do not match")); } + for y in 0..buf_dim_y { for x in 0..buf_dim_x { let video_pixel = video_buffer.get_pixel(x, y); @@ -151,10 +151,37 @@ fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), let video_pixel = gba_colour_to_rgba(video_pixel); let image_pixel = rgba_to_gba_to_rgba(image_pixel.0); if image_pixel != video_pixel { - return Err(anyhow!("images do not match")); + let output_file = write_video_buffer(video_buffer); + + return Err(anyhow!( + "images do not match, actual output written to {}", + output_file + )); } } } Ok(()) } + +fn write_video_buffer(video_buffer: &VideoBuffer) -> String { + let (width, height) = video_buffer.get_size(); + let mut output_image = image::DynamicImage::new_rgba8(width, height); + + for y in 0..height { + for x in 0..width { + let pixel = video_buffer.get_pixel(x, y); + let pixel_as_rgba = gba_colour_to_rgba(pixel); + + output_image.put_pixel(x, y, pixel_as_rgba.into()) + } + } + + let output_folder = std::env::temp_dir(); + let output_file = "mgba-test-runner-output.png"; // TODO make this random + + let output_file = output_folder.join(output_file); + let _ = output_image.save_with_format(&output_file, image::ImageFormat::Png); + + output_file.to_string_lossy().into_owned() +}