From a230d15ffcdb5f0f826a0533daa214e0ad89dbda Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 12 Aug 2025 19:23:49 -0500 Subject: [PATCH] test: setup common submodule, add text.rs tests, pattern exclude error.rs --- Justfile | 2 +- src/texture/text.rs | 90 ++++++++++++++++++++++-------------- tests/common/mod.rs | 39 ++++++++++++++++ tests/text.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 tests/common/mod.rs create mode 100644 tests/text.rs diff --git a/Justfile b/Justfile index 147611c..476a49d 100644 --- a/Justfile +++ b/Justfile @@ -1,7 +1,7 @@ set shell := ["bash", "-c"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] -coverage_exclude_pattern := "app.rs|audio.rs" +coverage_exclude_pattern := "app.rs|audio.rs|error.rs" # Display report (for humans) report-coverage: coverage diff --git a/src/texture/text.rs b/src/texture/text.rs index e568237..c6059dc 100644 --- a/src/texture/text.rs +++ b/src/texture/text.rs @@ -49,53 +49,74 @@ use glam::UVec2; use sdl2::render::{Canvas, RenderTarget}; use std::collections::HashMap; -use crate::texture::sprite::{AtlasTile, SpriteAtlas}; +use crate::{ + error::{GameError, TextureError}, + texture::sprite::{AtlasTile, SpriteAtlas}, +}; + +/// Converts a character to its tile name in the atlas. +fn char_to_tile_name(c: char) -> Option { + let name = match c { + // Letters A-Z + 'A'..='Z' | '0'..='9' => format!("text/{c}.png"), + // Special characters + '!' => "text/!.png".to_string(), + '-' => "text/-.png".to_string(), + '"' => "text/_double_quote.png".to_string(), + '/' => "text/_forward_slash.png".to_string(), + // Skip spaces for now - they don't have a tile + ' ' => return None, + + // Unsupported character + _ => return None, + }; + + Some(name) +} /// A text texture that renders characters from the atlas. +#[derive(Debug)] pub struct TextTexture { char_map: HashMap, scale: f32, } +impl Default for TextTexture { + fn default() -> Self { + Self { + scale: 1.0, + char_map: Default::default(), + } + } +} + impl TextTexture { - /// Creates a new text texture with the given atlas and scale. + /// Creates a new text texture with the given scale. pub fn new(scale: f32) -> Self { Self { - char_map: HashMap::new(), scale, + ..Default::default() } } - /// Maps a character to its atlas tile, handling special characters. - fn get_char_tile(&mut self, atlas: &SpriteAtlas, c: char) -> Option { - if let Some(tile) = self.char_map.get(&c) { - return Some(*tile); - } - - let tile_name = self.char_to_tile_name(c)?; - let tile = atlas.get_tile(&tile_name)?; - self.char_map.insert(c, tile); - Some(tile) + pub fn get_char_map(&self) -> &HashMap { + &self.char_map } - /// Converts a character to its tile name in the atlas. - fn char_to_tile_name(&self, c: char) -> Option { - let name = match c { - // Letters A-Z - 'A'..='Z' | '0'..='9' => format!("text/{c}.png"), - // Special characters - '!' => "text/!.png".to_string(), - '-' => "text/-.png".to_string(), - '"' => "text/_double_quote.png".to_string(), - '/' => "text/_forward_slash.png".to_string(), - // Skip spaces for now - they don't have a tile - ' ' => return None, + pub fn get_tile(&mut self, c: char, atlas: &mut SpriteAtlas) -> Result> { + if self.char_map.contains_key(&c) { + return Ok(self.char_map.get_mut(&c)); + } - // Unsupported character - _ => return None, - }; - - Some(name) + if let Some(tile_name) = char_to_tile_name(c) { + let tile = atlas + .get_tile(&tile_name) + .ok_or(GameError::Texture(TextureError::AtlasTileNotFound(tile_name)))?; + self.char_map.insert(c, tile); + Ok(self.char_map.get_mut(&c)) + } else { + Ok(None) + } } /// Renders a string of text at the given position. @@ -108,13 +129,16 @@ impl TextTexture { ) -> Result<()> { let mut x_offset = 0; let char_width = (8.0 * self.scale) as u32; - let char_height = (8.0 * self.scale) as u32; + let char_height = self.text_height(); for c in text.chars() { - if let Some(mut tile) = self.get_char_tile(atlas, c) { + // Get the tile from the char_map, or insert it if it doesn't exist + if let Some(tile) = self.get_tile(c, atlas)? { + // Render the tile if it exists let dest = sdl2::rect::Rect::new((position.x + x_offset) as i32, position.y as i32, char_width, char_height); tile.render(canvas, atlas, dest)?; } + // Always advance x_offset for all characters (including spaces) x_offset += char_width; } @@ -138,7 +162,7 @@ impl TextTexture { let mut width = 0; for c in text.chars() { - if self.char_to_tile_name(c).is_some() { + if char_to_tile_name(c).is_some() || c == ' ' { width += char_width; } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..a440287 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,39 @@ +#![allow(dead_code)] + +use pacman::{ + asset::{get_asset_bytes, Asset}, + texture::sprite::SpriteAtlas, +}; +use sdl2::{ + image::LoadTexture, + render::{Canvas, Texture, TextureCreator}, + video::{Window, WindowContext}, + Sdl, +}; + +pub fn setup_sdl() -> Result<(Canvas, TextureCreator, Sdl), String> { + let sdl_context = sdl2::init()?; + let video_subsystem = sdl_context.video()?; + let window = video_subsystem + .window("test", 800, 600) + .position_centered() + .hidden() + .build() + .map_err(|e| e.to_string())?; + let canvas = window.into_canvas().build().map_err(|e| e.to_string())?; + let texture_creator = canvas.texture_creator(); + Ok((canvas, texture_creator, sdl_context)) +} + +pub fn create_atlas(canvas: &mut sdl2::render::Canvas) -> SpriteAtlas { + let texture_creator = canvas.texture_creator(); + let atlas_bytes = get_asset_bytes(Asset::Atlas).unwrap(); + let atlas_json = get_asset_bytes(Asset::AtlasJson).unwrap(); + + let texture = texture_creator.load_texture_bytes(&atlas_bytes).unwrap(); + let texture: Texture<'static> = unsafe { std::mem::transmute(texture) }; + + let mapper: pacman::texture::sprite::AtlasMapper = serde_json::from_slice(&atlas_json).unwrap(); + + SpriteAtlas::new(texture, mapper) +} diff --git a/tests/text.rs b/tests/text.rs new file mode 100644 index 0000000..3e9664e --- /dev/null +++ b/tests/text.rs @@ -0,0 +1,109 @@ +use pacman::texture::{sprite::SpriteAtlas, text::TextTexture}; + +use crate::common::create_atlas; + +mod common; + +/// Helper function to get all characters that should be in the atlas +fn get_all_chars() -> String { + let mut chars = Vec::new(); + chars.extend('A'..='Z'); + chars.extend('0'..='9'); + chars.extend(['!', '-', '"', '/']); + chars.into_iter().collect() +} + +/// Helper function to check if a character is in the atlas and char_map +fn check_char(text_texture: &mut TextTexture, atlas: &mut SpriteAtlas, c: char) { + // Check that the character is not in the char_map yet + assert!( + !text_texture.get_char_map().contains_key(&c), + "Character {c} should not yet be in char_map" + ); + + // Get the tile from the atlas, which caches the tile in the char_map + let tile = text_texture.get_tile(c, atlas); + + assert!(tile.is_ok(), "Failed to get tile for character {c}"); + assert!(tile.unwrap().is_some(), "Tile for character {c} not found in atlas"); + + // Check that the tile is now cached in the char_map + assert!( + text_texture.get_char_map().contains_key(&c), + "Tile for character {c} was not cached in char_map" + ); +} + +#[test] +fn test_chars() -> Result<(), String> { + let (mut canvas, ..) = common::setup_sdl().map_err(|e| e.to_string())?; + let mut atlas = create_atlas(&mut canvas); + let mut text_texture = TextTexture::default(); + + get_all_chars() + .chars() + .for_each(|c| check_char(&mut text_texture, &mut atlas, c)); + + Ok(()) +} + +#[test] +fn test_render() -> Result<(), String> { + let (mut canvas, ..) = common::setup_sdl().map_err(|e| e.to_string())?; + let mut atlas = create_atlas(&mut canvas); + let mut text_texture = TextTexture::default(); + + let test_strings = vec!["Hello, world!".to_string(), get_all_chars()]; + + for string in test_strings { + if let Err(e) = text_texture.render(&mut canvas, &mut atlas, &string, glam::UVec2::new(0, 0)) { + return Err(e.to_string()); + } + } + + Ok(()) +} + +#[test] +fn test_text_width() -> Result<(), String> { + let text_texture = TextTexture::default(); + + let test_strings = vec!["Hello, world!".to_string(), get_all_chars()]; + + for string in test_strings { + let width = text_texture.text_width(&string); + let height = text_texture.text_height(); + + assert!(width > 0, "Width for string {string} should be greater than 0"); + assert!(height > 0, "Height for string {string} should be greater than 0"); + } + + Ok(()) +} + +#[test] +fn test_text_scale() -> Result<(), String> { + let string = "ABCDEFG !-/\""; + let base_width = (string.len() * 8) as u32; + + let mut text_texture = TextTexture::new(0.5); + + assert_eq!(text_texture.scale(), 0.5); + assert_eq!(text_texture.text_height(), 4); + assert_eq!(text_texture.text_width(""), 0); + assert_eq!(text_texture.text_width(string), base_width / 2); + + text_texture.set_scale(2.0); + assert_eq!(text_texture.scale(), 2.0); + assert_eq!(text_texture.text_height(), 16); + assert_eq!(text_texture.text_width(string), base_width * 2); + assert_eq!(text_texture.text_width(""), 0); + + text_texture.set_scale(1.0); + assert_eq!(text_texture.scale(), 1.0); + assert_eq!(text_texture.text_height(), 8); + assert_eq!(text_texture.text_width(string), base_width); + assert_eq!(text_texture.text_width(""), 0); + + Ok(()) +}