gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The pictures of the asset repository as this interface has them: asked
//! from the engine once per path and kept, so every cache that draws one
//! decodes the same bytes. A picture chosen here is kept as well, and
//! stored in the repository through the engine.

use std::{
    cell::RefCell,
    collections::HashMap,
    path::{Path, PathBuf},
    sync::Arc,
};

use gled_model::asset::picture::is_picture_path;
use gled_protocol::{bytes::SharedBytes, state::ClientInput};

/// A picture's bytes, or why there are none.
pub(crate) type Picture = Result<SharedBytes, String>;

thread_local! {
    /// Every picture asked for: `None` while the engine's answer is out.
    static PICTURES: RefCell<HashMap<PathBuf, Option<Picture>>> = RefCell::new(HashMap::new());
}

/// The picture at `path` in the asset repository, once it is here. The
/// first call asks the engine for it. A path outside the repository, as
/// files saved before pictures moved into it may still name, has none.
pub(crate) fn get(path: &Path) -> Option<Picture> {
    if !is_picture_path(path) {
        return Some(Err(format!(
            "{} is not in the asset repository",
            path.display()
        )));
    }
    PICTURES.with(|pictures| {
        let mut pictures = pictures.borrow_mut();
        match pictures.get(path) {
            Some(answer) => answer.clone(),
            None => {
                pictures.insert(path.to_path_buf(), None);
                ClientInput::RequestPicture(path.to_path_buf()).enqueue();
                None
            }
        }
    })
}

/// Takes in a picture the engine sent.
pub(crate) fn receive(path: PathBuf, picture: Picture) {
    PICTURES.with(|pictures| pictures.borrow_mut().insert(path, Some(picture)));
}

/// Keeps a picture chosen here as the one at `path`, and stores it in the
/// asset repository.
pub(crate) fn store(path: PathBuf, bytes: Vec<u8>) {
    let bytes = SharedBytes(Arc::new(bytes));
    receive(path.clone(), Ok(bytes.clone()));
    ClientInput::SavePicture { path, bytes }.enqueue();
}

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

    #[test]
    fn a_stored_picture_is_here_at_once() {
        let path = PathBuf::from("pictures/stored.png");
        store(path.clone(), b"png".to_vec());
        assert_eq!(
            get(&path)
                .and_then(Result::ok)
                .map(|bytes| bytes.0.to_vec()),
            Some(b"png".to_vec())
        );
    }

    #[test]
    fn a_picture_is_asked_for_once_and_then_waited_for() {
        let path = PathBuf::from("pictures/asked.png");
        assert_eq!(get(&path), None);
        assert_eq!(get(&path), None);
        receive(path.clone(), Err("gone".to_owned()));
        assert_eq!(get(&path), Some(Err("gone".to_owned())));
    }

    #[test]
    fn a_path_outside_the_repository_has_no_picture() {
        assert!(matches!(
            get(Path::new("/home/someone/stage.png")),
            Some(Err(_))
        ));
    }
}