diff --git a/CHANGELOG.md b/CHANGELOG.md index 612cf105..11497da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Text renderer can now be re-used which is useful for rpg style character/word at a time text boxes. + ## [0.12.2] - 2022/10/22 This is a minor release to fix an alignment issue with background tiles. diff --git a/agb/examples/font/font-test-output.png b/agb/examples/font/font-test-output.png index b07074d5..be7bce8b 100644 Binary files a/agb/examples/font/font-test-output.png and b/agb/examples/font/font-test-output.png differ diff --git a/agb/examples/mixer_32768.rs b/agb/examples/mixer_32768.rs index 2d9f60d7..a37aca62 100644 --- a/agb/examples/mixer_32768.rs +++ b/agb/examples/mixer_32768.rs @@ -27,7 +27,8 @@ fn main(mut gba: Gba) -> ! { init_background(&mut bg, &mut vram); - let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 0, &mut bg, &mut vram); + let mut title_renderer = FONT.render_text((0u16, 3u16).into()); + let mut writer = title_renderer.writer(1, 0, &mut bg, &mut vram); writeln!(&mut writer, "Crazy Glue by Josh Woodward").unwrap(); @@ -51,6 +52,7 @@ fn main(mut gba: Gba) -> ! { let mut frame_counter = 0i32; let mut has_written_frame_time = false; + let mut stats_renderer = FONT.render_text((0u16, 6u16).into()); loop { vblank_provider.wait_for_vblank(); bg.commit(&mut vram); @@ -66,7 +68,9 @@ fn main(mut gba: Gba) -> ! { let percent = (total_cycles * 100) / 280896; - let mut writer = FONT.render_text((0u16, 6u16).into(), 2, 0, &mut bg, &mut vram); + stats_renderer.clear(&mut vram); + + let mut writer = stats_renderer.writer(1, 0, &mut bg, &mut vram); writeln!(&mut writer, "{total_cycles} cycles").unwrap(); writeln!(&mut writer, "{percent} percent").unwrap(); diff --git a/agb/examples/stereo_sound.rs b/agb/examples/stereo_sound.rs index 66dbcdee..1af3eb6c 100644 --- a/agb/examples/stereo_sound.rs +++ b/agb/examples/stereo_sound.rs @@ -27,10 +27,12 @@ fn main(mut gba: Gba) -> ! { init_background(&mut bg, &mut vram); - let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 0, &mut bg, &mut vram); + let mut title_renderer = FONT.render_text((0u16, 3u16).into()); + let mut writer = title_renderer.writer(1, 0, &mut bg, &mut vram); writeln!(&mut writer, "Let it in by Josh Woodward").unwrap(); + writer.commit(); bg.commit(&mut vram); @@ -49,6 +51,8 @@ fn main(mut gba: Gba) -> ! { let mut frame_counter = 0i32; let mut has_written_frame_time = false; + + let mut stats_renderer = FONT.render_text((0u16, 6u16).into()); loop { vblank_provider.wait_for_vblank(); bg.commit(&mut vram); @@ -65,7 +69,7 @@ fn main(mut gba: Gba) -> ! { let percent = (total_cycles * 100) / 280896; - let mut writer = FONT.render_text((0u16, 6u16).into(), 2, 0, &mut bg, &mut vram); + let mut writer = stats_renderer.writer(1, 0, &mut bg, &mut vram); writeln!(&mut writer, "{total_cycles} cycles").unwrap(); writeln!(&mut writer, "{percent} percent").unwrap(); diff --git a/agb/examples/text_render.rs b/agb/examples/text_render.rs index 79470d3d..9f38f6b5 100644 --- a/agb/examples/text_render.rs +++ b/agb/examples/text_render.rs @@ -40,7 +40,8 @@ fn main(mut gba: agb::Gba) -> ! { vram.remove_dynamic_tile(background_tile); - let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram); + let mut renderer = FONT.render_text((0u16, 3u16).into()); + let mut writer = renderer.writer(1, 2, &mut bg, &mut vram); writeln!(&mut writer, "Hello, World!").unwrap(); writeln!(&mut writer, "This is a font rendering example").unwrap(); @@ -53,7 +54,8 @@ fn main(mut gba: agb::Gba) -> ! { let mut frame = 0; loop { - let mut writer = FONT.render_text((4u16, 0u16).into(), 1, 2, &mut bg, &mut vram); + let mut renderer = FONT.render_text((4u16, 0u16).into()); + let mut writer = renderer.writer(1, 2, &mut bg, &mut vram); writeln!(&mut writer, "Frame {}", frame).unwrap(); writer.commit(); @@ -62,5 +64,7 @@ fn main(mut gba: agb::Gba) -> ! { vblank.wait_for_vblank(); bg.commit(&mut vram); + + renderer.clear(&mut vram); } } diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index ac105391..7f4df874 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -5,6 +5,10 @@ use crate::hash_map::HashMap; use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager}; +/// The text renderer renders a variable width fixed size +/// bitmap font using dynamic tiles as a rendering surface. +/// Does not support any unicode features. +/// For usage see the `text_render.rs` example pub struct FontLetter { width: u8, height: u8, @@ -57,69 +61,91 @@ impl Font { } impl Font { - pub fn render_text<'a>( - &'a self, - tile_pos: Vector2D, - foreground_colour: u8, - background_colour: u8, - bg: &'a mut RegularMap, - vram_manager: &'a mut VRamManager, - ) -> TextRenderer<'a> { + #[must_use] + /// Create renderer starting at the given tile co-ordinates. + pub fn render_text(&self, tile_pos: Vector2D) -> TextRenderer<'_> { TextRenderer { current_x_pos: 0, current_y_pos: 0, font: self, tile_pos, - vram_manager, - bg, - background_colour, - foreground_colour, tiles: Default::default(), } } } +/// Keeps track of the cursor and manages rendered tiles. pub struct TextRenderer<'a> { current_x_pos: i32, current_y_pos: i32, font: &'a Font, tile_pos: Vector2D, - vram_manager: &'a mut VRamManager, - bg: &'a mut RegularMap, - background_colour: u8, - foreground_colour: u8, tiles: HashMap<(i32, i32), DynamicTile<'a>>, } -impl<'a> Write for TextRenderer<'a> { +/// Generated from the renderer for use +/// with `Write` trait methods. +pub struct TextWriter<'a, 'b> { + foreground_colour: u8, + background_colour: u8, + text_renderer: &'a mut TextRenderer<'b>, + vram_manager: &'a mut VRamManager, + bg: &'a mut RegularMap, +} + +impl<'a, 'b> Write for TextWriter<'a, 'b> { fn write_str(&mut self, text: &str) -> Result<(), Error> { for c in text.chars() { - if c == '\n' { - self.current_y_pos += self.font.line_height; - self.current_x_pos = 0; - continue; - } - - let letter = self.font.letter(c); - - self.render_letter(letter); - - self.current_x_pos += i32::from(letter.advance_width); + self.text_renderer.write_char( + c, + self.vram_manager, + self.foreground_colour, + self.background_colour, + ); } Ok(()) } } +impl<'a, 'b> TextWriter<'a, 'b> { + pub fn commit(self) { + self.text_renderer.commit(self.bg, self.vram_manager); + } +} + fn div_ceil(quotient: i32, divisor: i32) -> i32 { (quotient + divisor - 1) / divisor } -impl<'a> TextRenderer<'a> { - fn render_letter(&mut self, letter: &FontLetter) { - let vram_manager = &mut self.vram_manager; - let foreground_colour = self.foreground_colour; - let background_colour = self.background_colour; +impl<'a, 'b> TextRenderer<'b> { + pub fn writer( + &'a mut self, + foreground_colour: u8, + background_colour: u8, + bg: &'a mut RegularMap, + vram_manager: &'a mut VRamManager, + ) -> TextWriter<'a, 'b> { + TextWriter { + text_renderer: self, + foreground_colour, + background_colour, + bg, + vram_manager, + } + } + + /// Renders a single character creating as many dynamic tiles as needed. + /// The foreground and background colour are palette indicies. + fn render_letter( + &mut self, + letter: &FontLetter, + vram_manager: &mut VRamManager, + foreground_colour: u8, + background_colour: u8, + ) { + assert!(foreground_colour < 16); + assert!(background_colour < 16); let x_start = (self.current_x_pos + i32::from(letter.xmin)).max(0); let y_start = self.current_y_pos + self.font.ascent @@ -182,27 +208,44 @@ impl<'a> TextRenderer<'a> { } } - pub fn commit(mut self) { - let tiles = core::mem::take(&mut self.tiles); - - for ((x, y), tile) in tiles.into_iter() { - self.bg.set_tile( - self.vram_manager, - (self.tile_pos.x + x as u16, self.tile_pos.y + y as u16).into(), + /// Commit the dynamic tiles that contain the text to the background. + pub fn commit(&self, bg: &'a mut RegularMap, vram_manager: &'a mut VRamManager) { + for ((x, y), tile) in self.tiles.iter() { + bg.set_tile( + vram_manager, + (self.tile_pos.x + *x as u16, self.tile_pos.y + *y as u16).into(), &tile.tile_set(), TileSetting::from_raw(tile.tile_index()), ); - self.vram_manager.remove_dynamic_tile(tile); } } -} -impl<'a> Drop for TextRenderer<'a> { - fn drop(&mut self) { + /// Write another char into the text, moving the cursor as appropriate. + pub fn write_char( + &mut self, + c: char, + vram_manager: &mut VRamManager, + foreground_colour: u8, + background_colour: u8, + ) { + if c == '\n' { + self.current_y_pos += self.font.line_height; + self.current_x_pos = 0; + } else { + let letter = self.font.letter(c); + self.render_letter(letter, vram_manager, foreground_colour, background_colour); + self.current_x_pos += i32::from(letter.advance_width); + } + } + + /// Clear the text, removing the tiles from vram and resetting the cursor. + pub fn clear(&mut self, vram_manager: &mut VRamManager) { + self.current_x_pos = 0; + self.current_y_pos = 0; let tiles = core::mem::take(&mut self.tiles); for (_, tile) in tiles.into_iter() { - self.vram_manager.remove_dynamic_tile(tile); + vram_manager.remove_dynamic_tile(tile); } } } @@ -242,16 +285,30 @@ mod tests { vram.remove_dynamic_tile(background_tile); - let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram); + let mut renderer = FONT.render_text((0u16, 3u16).into()); - writeln!(&mut writer, "Hello, World!").unwrap(); - writeln!(&mut writer, "This is a font rendering example").unwrap(); + // Test twice to ensure that clearing works + for _ in 0..2 { + let mut writer = renderer.writer(1, 2, &mut bg, &mut vram); + write!(&mut writer, "Hello, ").unwrap(); + writer.commit(); - writer.commit(); + // Test changing color + let mut writer = renderer.writer(4, 2, &mut bg, &mut vram); + writeln!(&mut writer, "World!").unwrap(); + writer.commit(); + bg.commit(&mut vram); + bg.show(); - bg.commit(&mut vram); - bg.show(); + // Test writing with same renderer after showing background + let mut writer = renderer.writer(1, 2, &mut bg, &mut vram); + writeln!(&mut writer, "This is a font rendering example").unwrap(); + writer.commit(); + bg.commit(&mut vram); + bg.show(); - crate::test_runner::assert_image_output("examples/font/font-test-output.png"); + crate::test_runner::assert_image_output("examples/font/font-test-output.png"); + renderer.clear(&mut vram); + } } }