use crate::error::{PlatformError, Result};
use std::path::Path;
#[derive(Debug, Clone)]
pub enum AssetPath {
Relative(String),
Absolute(String),
Embedded(&'static str),
}
impl AssetPath {
pub fn relative(path: impl Into<String>) -> Self {
Self::Relative(path.into())
}
pub fn absolute(path: impl Into<String>) -> Self {
Self::Absolute(path.into())
}
pub const fn embedded(name: &'static str) -> Self {
Self::Embedded(name)
}
}
impl<S: Into<String>> From<S> for AssetPath {
fn from(s: S) -> Self {
let s = s.into();
if s.starts_with('/') || (cfg!(windows) && s.chars().nth(1) == Some(':')) {
Self::Absolute(s)
} else {
Self::Relative(s)
}
}
}
pub trait AssetLoader: Send + Sync {
fn load(&self, path: &AssetPath) -> Result<Vec<u8>>;
fn exists(&self, path: &AssetPath) -> bool;
fn load_string(&self, path: &AssetPath) -> Result<String> {
let bytes = self.load(path)?;
String::from_utf8(bytes)
.map_err(|e| PlatformError::AssetLoad(format!("Invalid UTF-8: {}", e)))
}
fn platform_name(&self) -> &'static str;
}
#[derive(Debug, Clone)]
pub struct FilesystemAssetLoader {
base_path: Option<std::path::PathBuf>,
}
impl FilesystemAssetLoader {
pub fn new() -> Self {
Self { base_path: None }
}
pub fn with_base_path(base: impl AsRef<Path>) -> Self {
Self {
base_path: Some(base.as_ref().to_path_buf()),
}
}
pub fn set_base_path(&mut self, base: impl AsRef<Path>) {
self.base_path = Some(base.as_ref().to_path_buf());
}
fn resolve_path(&self, path: &AssetPath) -> std::path::PathBuf {
match path {
AssetPath::Relative(rel) => {
if let Some(ref base) = self.base_path {
base.join(rel)
} else {
std::path::PathBuf::from(rel)
}
}
AssetPath::Absolute(abs) => std::path::PathBuf::from(abs),
AssetPath::Embedded(name) => {
if let Some(ref base) = self.base_path {
base.join(name)
} else {
std::path::PathBuf::from(*name)
}
}
}
}
}
impl Default for FilesystemAssetLoader {
fn default() -> Self {
Self::new()
}
}
impl AssetLoader for FilesystemAssetLoader {
fn load(&self, path: &AssetPath) -> Result<Vec<u8>> {
let resolved = self.resolve_path(path);
std::fs::read(&resolved).map_err(|e| {
PlatformError::AssetLoad(format!("Failed to load '{}': {}", resolved.display(), e))
})
}
fn exists(&self, path: &AssetPath) -> bool {
let resolved = self.resolve_path(path);
resolved.exists()
}
fn platform_name(&self) -> &'static str {
"filesystem"
}
}
static GLOBAL_LOADER: std::sync::OnceLock<Box<dyn AssetLoader>> = std::sync::OnceLock::new();
pub fn set_global_asset_loader(loader: Box<dyn AssetLoader>) -> Result<()> {
GLOBAL_LOADER.set(loader).map_err(|_| {
PlatformError::InitFailed("Global asset loader already initialized".to_string())
})
}
pub fn global_asset_loader() -> Option<&'static dyn AssetLoader> {
GLOBAL_LOADER.get().map(|b| b.as_ref())
}
pub fn load_asset(path: impl Into<AssetPath>) -> Result<Vec<u8>> {
let loader = global_asset_loader()
.ok_or_else(|| PlatformError::AssetLoad("No asset loader configured".to_string()))?;
loader.load(&path.into())
}
pub fn asset_exists(path: impl Into<AssetPath>) -> bool {
global_asset_loader()
.map(|l| l.exists(&path.into()))
.unwrap_or(false)
}
pub fn load_asset_string(path: impl Into<AssetPath>) -> Result<String> {
let loader = global_asset_loader()
.ok_or_else(|| PlatformError::AssetLoad("No asset loader configured".to_string()))?;
loader.load_string(&path.into())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_filesystem_loader() {
let temp_dir = std::env::temp_dir();
let test_file = temp_dir.join("blinc_test_asset.txt");
let mut f = std::fs::File::create(&test_file).unwrap();
f.write_all(b"Hello, Blinc!").unwrap();
let loader = FilesystemAssetLoader::new();
let path = AssetPath::Absolute(test_file.to_string_lossy().to_string());
let data = loader.load(&path).unwrap();
assert_eq!(data, b"Hello, Blinc!");
assert!(loader.exists(&path));
std::fs::remove_file(test_file).unwrap();
}
#[test]
fn test_asset_path_from_string() {
let relative: AssetPath = "images/logo.png".into();
assert!(matches!(relative, AssetPath::Relative(_)));
let absolute: AssetPath = "/absolute/path.png".into();
assert!(matches!(absolute, AssetPath::Absolute(_)));
}
}