Compare commits

..

1 Commits

4 changed files with 206 additions and 34 deletions

View File

@@ -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

View File

@@ -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<String> {
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<char, AtlasTile>,
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<AtlasTile> {
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<char, AtlasTile> {
&self.char_map
}
/// Converts a character to its tile name in the atlas.
fn char_to_tile_name(&self, c: char) -> Option<String> {
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<Option<&mut AtlasTile>> {
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;
}
}

39
tests/common/mod.rs Normal file
View File

@@ -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<Window>, TextureCreator<WindowContext>, 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<sdl2::video::Window>) -> 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)
}

109
tests/text.rs Normal file
View File

@@ -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(())
}