use std::{
collections::HashMap,
ffi::OsStr,
io,
path::{Path, PathBuf},
};
use topcoat_core::context::{Cx, app_context};
use crate::{Asset, MANIFEST_NAME, Manifest};
#[derive(Debug, Clone)]
pub struct BundledAsset {
path: PathBuf,
content_type: String,
}
impl BundledAsset {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn name(&self) -> &OsStr {
self.path
.file_name()
.expect("asset file path must have a name")
}
#[must_use]
pub fn content_type(&self) -> &str {
&self.content_type
}
}
#[derive(Debug, Default, Clone)]
pub struct AssetBundle {
dir: PathBuf,
bundled_assets: HashMap<Asset, BundledAsset>,
}
impl AssetBundle {
#[must_use]
pub fn empty() -> Self {
AssetBundle::default()
}
pub fn load() -> io::Result<Self> {
let exe = std::env::current_exe()?;
let exe_dir = exe
.parent()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"current executable has no parent directory",
)
})?
.to_path_buf();
let mut tried = Vec::new();
let mut current = Some(exe_dir.as_path());
for _ in 0..6 {
let Some(d) = current else { break };
let candidate = d.join("assets");
if candidate.join(MANIFEST_NAME).is_file() {
return Self::load_dir(candidate);
}
tried.push(candidate);
if d.file_name() == Some(OsStr::new("target")) {
break;
}
current = d.parent();
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"no asset bundle found near {}: tried {}",
exe_dir.display(),
tried
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
))
}
pub fn load_dir(dir: impl AsRef<Path>) -> io::Result<Self> {
let dir = dir.as_ref().to_path_buf();
let manifest = Manifest::load(dir.join(MANIFEST_NAME))?;
let bundled_assets = manifest
.assets
.into_iter()
.map(|entry| {
(
entry.id,
BundledAsset {
path: dir.join(entry.file),
content_type: entry.content_type,
},
)
})
.collect();
Ok(Self {
dir,
bundled_assets,
})
}
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
#[must_use]
pub fn get(&self, id: Asset) -> Option<&BundledAsset> {
self.bundled_assets.get(&id)
}
pub fn assets(&self) -> impl Iterator<Item = &BundledAsset> {
self.bundled_assets.values()
}
}
#[must_use]
pub fn asset_bundle(cx: &Cx) -> &AssetBundle {
app_context(cx)
}
#[must_use]
pub fn bundled_asset(cx: &Cx, asset: Asset) -> &BundledAsset {
match asset_bundle(cx).get(asset) {
Some(asset) => asset,
None => panic!("failed to resolve asset {asset:?} in app context's asset bundle"),
}
}