use alloc::borrow::Cow;
use alloc::boxed::Box;
use crate::app::plugin::Plugin;
use crate::app::{App, RendererFactory};
use crate::core::cache::MaxSize;
use crate::core::resource::ResourceManager;
use crate::render::texture::{ColorFormat, Texture, TextureMeta};
use crate::surface::Surface;
use crate::ui::widgets::assets::IMG_THUMBS_UP;
const DEFAULT_VALUES_BUDGET: MaxSize = MaxSize::Bytes(256 * 1024);
const DEFAULT_PROBES_BUDGET: MaxSize = MaxSize::Count(1024);
static CHECKERBOARD_PIXELS: [u8; 16 * 16 * 4] = checkerboard_pixels();
const fn checkerboard_pixels() -> [u8; 16 * 16 * 4] {
let mut buf = [0u8; 16 * 16 * 4];
let mut y = 0;
while y < 16 {
let mut x = 0;
while x < 16 {
let i = (y * 16 + x) * 4;
let on = ((x / 4) ^ (y / 4)) & 1 == 0;
if on {
buf[i] = 0xFF; buf[i + 2] = 0xFF; }
buf[i + 3] = 0xFF; x += 1;
}
y += 1;
}
buf
}
static FALLBACK_TEXTURE: Texture<'static> = Texture {
buf: crate::render::texture::TexBuf::Ref(&CHECKERBOARD_PIXELS),
width: 16,
height: 16,
format: ColorFormat::RGBA8888,
stride: 64,
alpha_mode: crate::render::texture::AlphaMode::Opaque,
transient: false,
};
const FALLBACK_META: TextureMeta = TextureMeta {
width: 16,
height: 16,
format: ColorFormat::RGBA8888,
};
type StaticEntry = (Cow<'static, str>, Texture<'static>);
type MirxEntry = (Cow<'static, str>, &'static [u8]);
type LoaderEntry = Box<dyn FnOnce(&ResourceManager<Texture<'static>>) + 'static>;
pub struct ImageResourcesPlugin {
values_budget: MaxSize,
probes_budget: MaxSize,
bundle_thumbs_up: bool,
statics: alloc::vec::Vec<StaticEntry>,
mirx_bytes: alloc::vec::Vec<MirxEntry>,
loaders: alloc::vec::Vec<LoaderEntry>,
}
impl Default for ImageResourcesPlugin {
fn default() -> Self {
Self {
values_budget: DEFAULT_VALUES_BUDGET,
probes_budget: DEFAULT_PROBES_BUDGET,
bundle_thumbs_up: true,
statics: alloc::vec::Vec::new(),
mirx_bytes: alloc::vec::Vec::new(),
loaders: alloc::vec::Vec::new(),
}
}
}
impl ImageResourcesPlugin {
pub fn empty() -> Self {
Self {
bundle_thumbs_up: false,
..Self::default()
}
}
pub fn with_values_budget(mut self, budget: MaxSize) -> Self {
self.values_budget = budget;
self
}
pub fn with_probes_budget(mut self, budget: MaxSize) -> Self {
self.probes_budget = budget;
self
}
pub fn with_static(
mut self,
token: impl Into<Cow<'static, str>>,
texture: Texture<'static>,
) -> Self {
self.statics.push((token.into(), texture));
self
}
pub fn with_mirx_bytes(
mut self,
token: impl Into<Cow<'static, str>>,
bytes: &'static [u8],
) -> Self {
self.mirx_bytes.push((token.into(), bytes));
self
}
pub fn with_loader<L>(mut self, configure: L) -> Self
where
L: FnOnce(&ResourceManager<Texture<'static>>) + 'static,
{
self.loaders.push(Box::new(configure));
self
}
}
impl<B, F> Plugin<B, F> for ImageResourcesPlugin
where
B: Surface,
F: RendererFactory<B>,
{
fn build(&mut self, app: &mut App<B, F>) {
let manager =
ResourceManager::<Texture<'static>>::new(self.values_budget, FALLBACK_TEXTURE.clone())
.with_probes(self.probes_budget, FALLBACK_META);
if self.bundle_thumbs_up {
manager.add_static("thumbs_up", IMG_THUMBS_UP.clone());
}
for (token, tex) in self.statics.drain(..) {
manager.add_static(token, tex);
}
for (token, bytes) in self.mirx_bytes.drain(..) {
if let Err(e) = manager.add_mirx_bytes(token.clone(), bytes) {
panic!("ImageResourcesPlugin: bad mirx bytes for {token:?}: {e:?}");
}
}
for configure in self.loaders.drain(..) {
configure(&manager);
}
app.world.insert_resource(manager);
}
fn name(&self) -> &'static str {
"ImageResourcesPlugin"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::resource::HasProbe;
fn rebuild_into_manager(
plugin: &mut ImageResourcesPlugin,
) -> ResourceManager<Texture<'static>> {
let manager = ResourceManager::<Texture<'static>>::new(
plugin.values_budget,
FALLBACK_TEXTURE.clone(),
)
.with_probes(plugin.probes_budget, FALLBACK_META);
if plugin.bundle_thumbs_up {
manager.add_static("thumbs_up", IMG_THUMBS_UP.clone());
}
for (token, tex) in plugin.statics.drain(..) {
manager.add_static(token, tex);
}
for (token, bytes) in plugin.mirx_bytes.drain(..) {
manager.add_mirx_bytes(token, bytes).unwrap();
}
for configure in plugin.loaders.drain(..) {
configure(&manager);
}
manager
}
fn build_flat_rgb565_2x1() -> &'static [u8] {
let input = mirx::FlatImageInput {
width: 2,
height: 1,
stride: 4,
format: mirx::ColorFormat::RGB565,
main: &[0xAA, 0xBB, 0xCC, 0xDD],
extra: None,
};
Box::leak(mirx::encode_flat(&input).into_boxed_slice())
}
#[test]
fn default_bundles_thumbs_up() {
let mut plugin = ImageResourcesPlugin::default();
let manager = rebuild_into_manager(&mut plugin);
let tex = manager.resolve("thumbs_up");
assert_eq!(tex.width, 16);
assert_eq!(tex.format, ColorFormat::RGBA8888);
}
#[test]
fn empty_skips_thumbs_up() {
let mut plugin = ImageResourcesPlugin::empty();
let manager = rebuild_into_manager(&mut plugin);
assert_eq!(manager.probe("thumbs_up"), None);
assert_eq!(manager.resolve("thumbs_up").extract_meta(), FALLBACK_META);
}
#[test]
fn unknown_token_falls_back_to_checkerboard() {
let mut plugin = ImageResourcesPlugin::default();
let manager = rebuild_into_manager(&mut plugin);
let tex = manager.resolve("nope");
assert_eq!(tex.extract_meta(), FALLBACK_META);
}
#[test]
fn with_static_adds_token() {
let extra = IMG_THUMBS_UP.clone();
let mut plugin = ImageResourcesPlugin::default().with_static("alt_logo", extra);
let manager = rebuild_into_manager(&mut plugin);
let tex = manager.resolve("alt_logo");
assert_eq!(tex.width, 16);
}
#[test]
fn with_mirx_bytes_seeds_probe_and_value() {
let bytes = build_flat_rgb565_2x1();
let mut plugin = ImageResourcesPlugin::default().with_mirx_bytes("logo", bytes);
let manager = rebuild_into_manager(&mut plugin);
assert_eq!(
manager.probe("logo"),
Some(TextureMeta {
width: 2,
height: 1,
format: ColorFormat::RGB565,
})
);
assert_eq!(manager.resolve("logo").width, 2);
}
}