use crate::{Context, Image, Pt, Texture};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ImageAssetKey(String);
impl ImageAssetKey {
pub fn new(name: impl Into<String>) -> Self {
Self(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for ImageAssetKey {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for ImageAssetKey {
fn from(value: String) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ImageAsset {
texture: Texture,
version: u64,
}
impl ImageAsset {
pub fn image(self) -> Image {
self.texture.view()
}
pub fn texture(self) -> Texture {
self.texture
}
pub fn version(self) -> u64 {
self.version
}
}
#[derive(Debug, Default)]
pub struct ImageAssetStore {
assets: HashMap<ImageAssetKey, ImageAsset>,
next_version: u64,
}
impl ImageAssetStore {
pub fn new() -> Self {
Self::default()
}
pub fn replace_rgba8(
&mut self,
ctx: &mut Context,
key: impl Into<ImageAssetKey>,
width: u32,
height: u32,
rgba: Vec<u8>,
) -> anyhow::Result<u64> {
if width == 0 || height == 0 {
anyhow::bail!("image asset dimensions must be non-zero, got {width}x{height}");
}
let expected_len = (width as usize)
.checked_mul(height as usize)
.and_then(|pixels| pixels.checked_mul(4))
.ok_or_else(|| anyhow::anyhow!("image asset dimensions overflow: {width}x{height}"))?;
if rgba.len() != expected_len {
anyhow::bail!(
"image asset RGBA length mismatch: expected {expected_len}, got {}",
rgba.len()
);
}
let key = key.into();
if let Some(asset) = self.assets.get_mut(&key)
&& asset.texture.width().0 == width as f32
&& asset.texture.height().0 == height as f32
{
asset.texture.update_rgba8(ctx, rgba)?;
self.next_version = self.next_version.saturating_add(1).max(1);
asset.version = self.next_version;
return Ok(asset.version);
}
let texture = Texture::new(ctx, Pt::from(width as f32), Pt::from(height as f32), &rgba)?;
self.next_version = self.next_version.saturating_add(1).max(1);
let new_asset = ImageAsset {
texture,
version: self.next_version,
};
let old_asset = self.assets.insert(key, new_asset);
if let Some(old_asset) = old_asset {
old_asset.texture.destroy(ctx);
}
Ok(self.next_version)
}
pub fn get(&self, key: &str) -> Option<ImageAsset> {
self.assets.get(&ImageAssetKey::from(key)).copied()
}
pub fn remove(&mut self, ctx: &mut Context, key: &str) -> bool {
let Some(asset) = self.assets.remove(&ImageAssetKey::from(key)) else {
return false;
};
asset.texture.destroy(ctx)
}
pub fn clear(&mut self, ctx: &mut Context) {
let assets = std::mem::take(&mut self.assets);
for asset in assets.into_values() {
asset.texture.destroy(ctx);
}
}
pub fn len(&self) -> usize {
self.assets.len()
}
pub fn is_empty(&self) -> bool {
self.assets.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::ImageAssetStore;
use crate::{Context, Pt};
#[test]
fn replaces_same_sized_image_without_changing_texture_handle() {
let mut ctx = Context::new();
let mut store = ImageAssetStore::new();
let first_version = store
.replace_rgba8(&mut ctx, "player", 2, 2, vec![255; 2 * 2 * 4])
.expect("initial image should register");
assert_eq!(first_version, 1);
let first_texture = store
.get("player")
.expect("asset should be registered")
.texture();
assert_eq!(first_texture.width(), Pt::from(2.0));
let second_version = store
.replace_rgba8(&mut ctx, "player", 2, 2, vec![0; 2 * 2 * 4])
.expect("same-sized image should update");
assert_eq!(second_version, 2);
let second_texture = store
.get("player")
.expect("asset should remain registered")
.texture();
assert_eq!(first_texture.id(), second_texture.id());
}
#[test]
fn replacing_image_with_new_size_replaces_texture() {
let mut ctx = Context::new();
let mut store = ImageAssetStore::new();
store
.replace_rgba8(&mut ctx, "player", 1, 1, vec![255; 4])
.expect("initial image should register");
let first_texture = store.get("player").expect("asset should exist").texture();
store
.replace_rgba8(&mut ctx, "player", 2, 1, vec![0; 8])
.expect("resized image should register");
let second_texture = store.get("player").expect("asset should exist").texture();
assert_ne!(first_texture.id(), second_texture.id());
assert!(!first_texture.is_ready(&ctx));
}
#[test]
fn rejects_invalid_image_data_before_registering_asset() {
let mut ctx = Context::new();
let mut store = ImageAssetStore::new();
let error = store
.replace_rgba8(&mut ctx, "player", 2, 2, vec![0; 3])
.expect_err("invalid RGBA data should be rejected");
assert!(error.to_string().contains("RGBA length mismatch"));
assert!(store.is_empty());
}
}