godoru 0.1.0

UI Framework for Rust using Godot
use crate::{GodoruError, GodoruResult, Shader};
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetLoader {
    root: PathBuf,
}

impl AssetLoader {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    pub fn image(&self, path: impl AsRef<Path>) -> GodoruResult<ImageHandle> {
        let path = assetPath(path.as_ref())?;
        Ok(ImageHandle {
            path: self.root.join(path),
        })
    }

    pub fn font(&self, path: impl AsRef<Path>) -> GodoruResult<FontHandle> {
        let path = assetPath(path.as_ref())?;
        Ok(FontHandle {
            path: self.root.join(path),
        })
    }

    pub fn shader(&self, path: impl AsRef<Path>) -> GodoruResult<Shader> {
        let path = assetPath(path.as_ref())?;
        Ok(Shader::fromFile(self.root.join(path)))
    }
}

impl Default for AssetLoader {
    fn default() -> Self {
        Self::new("")
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageHandle {
    pub path: PathBuf,
}

impl ImageHandle {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    pub fn runtimePath(&self) -> String {
        runtimePath(&self.path)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FontHandle {
    pub path: PathBuf,
}

impl FontHandle {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    pub fn runtimePath(&self) -> String {
        runtimePath(&self.path)
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AssetKind {
    Image,
    Font,
    Shader,
}

fn assetPath(path: &Path) -> GodoruResult<PathBuf> {
    if path.as_os_str().is_empty() {
        Err(GodoruError::AssetPath("asset path is empty".to_string()))
    } else if path.is_absolute() {
        Err(GodoruError::AssetPath(format!(
            "asset path must be relative: {}",
            path.display()
        )))
    } else {
        Ok(path.to_path_buf())
    }
}

pub(crate) fn runtimePath(path: &Path) -> String {
    let mut value = String::from("res://assets");
    for item in path.components() {
        if let std::path::Component::Normal(item) = item {
            value.push('/');
            value.push_str(&item.to_string_lossy().replace('\\', "/"));
        }
    }
    value
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn assetRuntimePathUsesResAssets() {
        let image = AssetLoader::default().image("images/logo.webp").unwrap();

        assert_eq!(image.runtimePath(), "res://assets/images/logo.webp");
    }

    #[test]
    fn absoluteAssetsAreRejected() {
        let err = AssetLoader::default().image("/tmp/logo.png").unwrap_err();

        assert!(matches!(err, GodoruError::AssetPath(_)));
    }
}