extern crate alloc;
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use crate::fs::{AssetError, AssetRead, AssetSource, FsError};
use crate::image::{CacheHandle, ImageDescriptor, PixelFormat};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AssetPath {
Embedded(&'static str),
Fatfs(String),
Sim(String),
Memory(String),
}
impl AssetPath {
pub fn path_str(&self) -> &str {
match self {
AssetPath::Embedded(s) => s,
AssetPath::Fatfs(s) | AssetPath::Sim(s) | AssetPath::Memory(s) => s.as_str(),
}
}
pub fn source_kind(&self) -> &'static str {
match self {
AssetPath::Embedded(_) => "embedded",
AssetPath::Fatfs(_) => "fatfs",
AssetPath::Sim(_) => "sim",
AssetPath::Memory(_) => "memory",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AssetHandle(u32);
impl AssetHandle {
pub const fn as_u32(self) -> u32 {
self.0
}
}
struct CacheEntry {
handle: CacheHandle,
ts: u32,
desc: ImageDescriptor<'static>,
}
pub struct SlotCache<const N: usize> {
slots: [Option<CacheEntry>; N],
ts_counter: u32,
handle_counter: u32,
}
impl<const N: usize> SlotCache<N> {
pub const fn new() -> Self {
#[allow(clippy::declare_interior_mutable_const)]
const NONE_ENTRY: Option<CacheEntry> = None;
Self {
slots: [NONE_ENTRY; N],
ts_counter: 1,
handle_counter: 1,
}
}
pub fn get(&mut self, handle: CacheHandle) -> Option<&ImageDescriptor<'static>> {
let ts = self.ts_counter;
self.ts_counter = self.ts_counter.wrapping_add(1);
for entry in self.slots.iter_mut().flatten() {
if entry.handle == handle {
entry.ts = ts;
break;
}
}
for entry in self.slots.iter().flatten() {
if entry.handle == handle {
return Some(&entry.desc);
}
}
None
}
pub fn insert(&mut self, descriptor: ImageDescriptor<'static>) -> CacheHandle {
let handle = CacheHandle::new(self.handle_counter);
self.handle_counter = self.handle_counter.wrapping_add(1);
let ts = self.ts_counter;
self.ts_counter = self.ts_counter.wrapping_add(1);
for slot in &mut self.slots {
if slot.is_none() {
*slot = Some(CacheEntry {
handle,
ts,
desc: descriptor,
});
return handle;
}
}
let evict_idx = self
.slots
.iter()
.enumerate()
.filter_map(|(i, s)| s.as_ref().map(|e| (i, e.ts)))
.min_by_key(|&(_, t)| t)
.map(|(i, _)| i)
.expect("N >= 1 guaranteed by construction");
self.slots[evict_idx] = Some(CacheEntry {
handle,
ts,
desc: descriptor,
});
handle
}
pub fn evict(&mut self, handle: CacheHandle) {
for slot in &mut self.slots {
if slot.as_ref().is_some_and(|e| e.handle == handle) {
*slot = None;
return;
}
}
}
pub fn len(&self) -> usize {
self.slots.iter().filter(|s| s.is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl<const N: usize> Default for SlotCache<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> crate::image::ImageCache<'static> for SlotCache<N> {
fn get(&self, _handle: CacheHandle) -> Option<&ImageDescriptor<'static>> {
for entry in self.slots.iter().flatten() {
if entry.handle == _handle {
return Some(&entry.desc);
}
}
None
}
fn put(&mut self, descriptor: ImageDescriptor<'static>) -> CacheHandle {
self.insert(descriptor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourceKind {
Embedded,
Fatfs,
Sim,
Memory,
}
impl AssetPath {
fn kind(&self) -> SourceKind {
match self {
AssetPath::Embedded(_) => SourceKind::Embedded,
AssetPath::Fatfs(_) => SourceKind::Fatfs,
AssetPath::Sim(_) => SourceKind::Sim,
AssetPath::Memory(_) => SourceKind::Memory,
}
}
}
pub const ASSET_REGISTRY_MAX_SOURCES: usize = 4;
struct RegistrySource {
kind: SourceKind,
source: Box<dyn AssetSource>,
}
struct HandleRecord {
asset_handle: AssetHandle,
path: AssetPath,
cache_handle: Option<CacheHandle>,
}
pub struct AssetRegistry<const CACHE_SLOTS: usize = 8> {
sources: Vec<RegistrySource>,
handles: Vec<HandleRecord>,
cache: SlotCache<CACHE_SLOTS>,
next_asset_handle: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegistryError {
Full,
DuplicateKind,
}
impl<const CACHE_SLOTS: usize> AssetRegistry<CACHE_SLOTS> {
pub fn new() -> Self {
Self {
sources: Vec::new(),
handles: Vec::new(),
cache: SlotCache::new(),
next_asset_handle: 1,
}
}
pub fn register_source(
&mut self,
kind_sentinel: &AssetPath,
source: Box<dyn AssetSource>,
) -> Result<(), RegistryError> {
if self.sources.len() >= ASSET_REGISTRY_MAX_SOURCES {
return Err(RegistryError::Full);
}
let kind = kind_sentinel.kind();
if self.sources.iter().any(|s| s.kind == kind) {
return Err(RegistryError::DuplicateKind);
}
self.sources.push(RegistrySource { kind, source });
Ok(())
}
pub fn register(&mut self, path: AssetPath) -> AssetHandle {
let handle = AssetHandle(self.next_asset_handle);
self.next_asset_handle = self.next_asset_handle.wrapping_add(1);
self.handles.push(HandleRecord {
asset_handle: handle,
path,
cache_handle: None,
});
handle
}
pub fn resolve_image(
&mut self,
handle: AssetHandle,
) -> Result<&ImageDescriptor<'static>, AssetError> {
let record_idx = self
.handles
.iter()
.position(|r| r.asset_handle == handle)
.ok_or(AssetError::Fs(FsError::NoSuchFile))?;
let final_cache_handle: CacheHandle;
let existing_cache_handle: Option<CacheHandle> = self.handles[record_idx]
.cache_handle
.filter(|&ch| self.cache.slots.iter().flatten().any(|e| e.handle == ch));
if let Some(ch) = existing_cache_handle {
let _ = self.cache.get(ch);
final_cache_handle = ch;
} else {
self.handles[record_idx].cache_handle = None;
let path_str = self.handles[record_idx].path.path_str().to_owned();
let kind = self.handles[record_idx].path.kind();
let source_idx = self
.sources
.iter()
.position(|s| s.kind == kind)
.ok_or(AssetError::Fs(FsError::NoSuchFile))?;
let bytes = {
let source = &self.sources[source_idx].source;
let mut reader = source.open(&path_str)?;
let len = reader.len();
let mut buf = alloc::vec![0u8; len];
let mut off = 0usize;
while off < len {
let n = reader.read(&mut buf[off..])?;
if n == 0 {
break;
}
off += n;
}
buf
};
let desc = decode_bytes(&path_str, bytes)?;
let new_ch = self.cache.insert(desc);
self.handles[record_idx].cache_handle = Some(new_ch);
final_cache_handle = new_ch;
}
for e in self.cache.slots.iter().flatten() {
if e.handle == final_cache_handle {
return Ok(&e.desc);
}
}
Err(AssetError::Fs(FsError::Device))
}
}
impl<const CACHE_SLOTS: usize> Default for AssetRegistry<CACHE_SLOTS> {
fn default() -> Self {
Self::new()
}
}
fn decode_bytes(path: &str, bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
let lower = path.to_ascii_lowercase();
#[cfg(all(feature = "png", not(target_os = "none")))]
if lower.ends_with(".png") || bytes.get(..8).is_some_and(|h| h.starts_with(b"\x89PNG")) {
return decode_png(bytes);
}
#[cfg(all(feature = "jpeg", not(target_os = "none")))]
if lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| bytes.get(..2).is_some_and(|h| h == b"\xff\xd8")
{
return decode_jpeg(bytes);
}
#[cfg(feature = "gif")]
if lower.ends_with(".gif") || bytes.get(..4).is_some_and(|h| h == b"GIF8") {
return decode_gif(bytes);
}
let _ = lower; Ok(ImageDescriptor {
format: PixelFormat::Rgb565,
width: 0,
height: 0,
data: crate::image::ImageData::Owned(bytes),
stride: None,
})
}
#[cfg(all(feature = "png", not(target_os = "none")))]
fn decode_png(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
let (colors, w, h) = crate::plugins::png::decode(&bytes)
.map_err(|e| AssetError::Decode(alloc::format!("png: {e:?}")))?;
let mut pixels = Vec::with_capacity(colors.len() * 4);
for c in &colors {
pixels.push(c.0); pixels.push(c.1); pixels.push(c.2); pixels.push(c.3); }
Ok(ImageDescriptor {
format: PixelFormat::Argb8888,
width: w as u16,
height: h as u16,
data: crate::image::ImageData::Owned(pixels),
stride: None,
})
}
#[cfg(all(feature = "jpeg", not(target_os = "none")))]
fn decode_jpeg(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
let (colors, w, h) = crate::plugins::jpeg::decode(&bytes)
.map_err(|e| AssetError::Decode(alloc::format!("jpeg: {e:?}")))?;
let mut pixels = Vec::with_capacity(colors.len() * 4);
for c in &colors {
pixels.push(c.0);
pixels.push(c.1);
pixels.push(c.2);
pixels.push(c.3);
}
Ok(ImageDescriptor {
format: PixelFormat::Argb8888,
width: w,
height: h,
data: crate::image::ImageData::Owned(pixels),
stride: None,
})
}
#[cfg(feature = "gif")]
fn decode_gif(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
let (frames, w, h) = crate::plugins::gif::decode(&bytes)
.map_err(|e| AssetError::Decode(alloc::format!("gif: {e:?}")))?;
let frame = frames
.into_iter()
.next()
.ok_or_else(|| AssetError::Decode(alloc::string::String::from("gif: no frames")))?;
let mut pixels = Vec::with_capacity(frame.pixels.len() * 4);
for c in &frame.pixels {
pixels.push(c.0);
pixels.push(c.1);
pixels.push(c.2);
pixels.push(c.3);
}
Ok(ImageDescriptor {
format: PixelFormat::Argb8888,
width: w,
height: h,
data: crate::image::ImageData::Owned(pixels),
stride: None,
})
}
struct StaticSliceReader {
data: &'static [u8],
pos: usize,
}
impl AssetRead for StaticSliceReader {
fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
let remaining = self.data.len().saturating_sub(self.pos);
let n = out.len().min(remaining);
out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
fn len(&self) -> usize {
self.data.len()
}
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
self.pos = (pos as usize).min(self.data.len());
Ok(self.pos as u64)
}
}
pub struct EmbeddedAssetSource {
table: &'static [(&'static str, &'static [u8])],
}
impl EmbeddedAssetSource {
pub const fn new(table: &'static [(&'static str, &'static [u8])]) -> Self {
Self { table }
}
}
impl AssetSource for EmbeddedAssetSource {
fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
for &(name, bytes) in self.table {
if name == path {
return Ok(Box::new(StaticSliceReader {
data: bytes,
pos: 0,
}));
}
}
Err(AssetError::Fs(FsError::NoSuchFile))
}
fn exists(&self, path: &str) -> bool {
self.table.iter().any(|&(name, _)| name == path)
}
fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
Ok(crate::fs::AssetIter)
}
}
struct VecReader {
data: Vec<u8>,
pos: usize,
}
impl AssetRead for VecReader {
fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
let remaining = self.data.len().saturating_sub(self.pos);
let n = out.len().min(remaining);
out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
fn len(&self) -> usize {
self.data.len()
}
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
self.pos = (pos as usize).min(self.data.len());
Ok(self.pos as u64)
}
}
pub struct MemoryAssetSource {
entries: Vec<(String, Vec<u8>)>,
}
impl MemoryAssetSource {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn insert(&mut self, name: impl Into<String>, data: Vec<u8>) {
let name = name.into();
for entry in &mut self.entries {
if entry.0 == name {
entry.1 = data;
return;
}
}
self.entries.push((name, data));
}
}
impl Default for MemoryAssetSource {
fn default() -> Self {
Self::new()
}
}
impl AssetSource for MemoryAssetSource {
fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
for (name, data) in &self.entries {
if name == path {
return Ok(Box::new(VecReader {
data: data.clone(),
pos: 0,
}));
}
}
Err(AssetError::Fs(FsError::NoSuchFile))
}
fn exists(&self, path: &str) -> bool {
self.entries.iter().any(|(name, _)| name == path)
}
fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
Ok(crate::fs::AssetIter)
}
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
pub struct SimAssetSource {
prefix: std::path::PathBuf,
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
impl SimAssetSource {
pub fn new(prefix: impl Into<std::path::PathBuf>) -> Self {
Self {
prefix: prefix.into(),
}
}
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
struct StdFileReader {
data: Vec<u8>,
pos: usize,
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
impl AssetRead for StdFileReader {
fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
let remaining = self.data.len().saturating_sub(self.pos);
let n = out.len().min(remaining);
out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
fn len(&self) -> usize {
self.data.len()
}
fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
self.pos = (pos as usize).min(self.data.len());
Ok(self.pos as u64)
}
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
impl AssetSource for SimAssetSource {
fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
use std::io::Read as _;
let full = self.prefix.join(path);
let mut file = std::fs::File::open(&full).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
AssetError::Fs(FsError::NoSuchFile)
} else {
AssetError::Fs(FsError::Device)
}
})?;
let mut data = Vec::new();
file.read_to_end(&mut data)
.map_err(|_| AssetError::Fs(FsError::Device))?;
Ok(Box::new(StdFileReader { data, pos: 0 }))
}
fn exists(&self, path: &str) -> bool {
self.prefix.join(path).exists()
}
fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
Ok(crate::fs::AssetIter)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::{ImageData, PixelFormat};
static PIXEL_2X1: &[u8] = &[0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF]; static EMBED_TABLE: &[(&str, &[u8])] = &[("icons/red_green.raw", PIXEL_2X1)];
#[test]
fn embedded_source_open_hit() {
let src = EmbeddedAssetSource::new(EMBED_TABLE);
assert!(src.exists("icons/red_green.raw"));
let mut reader = src.open("icons/red_green.raw").unwrap();
assert_eq!(reader.len(), 8);
let mut buf = [0u8; 8];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 8);
assert_eq!(&buf, PIXEL_2X1);
}
#[test]
fn embedded_source_open_miss() {
let src = EmbeddedAssetSource::new(EMBED_TABLE);
assert!(!src.exists("missing.raw"));
let result = src.open("missing.raw");
assert!(matches!(result, Err(AssetError::Fs(FsError::NoSuchFile))));
}
#[test]
fn memory_source_round_trip() {
let mut src = MemoryAssetSource::new();
src.insert("test.raw", vec![1, 2, 3, 4]);
assert!(src.exists("test.raw"));
let mut reader = src.open("test.raw").unwrap();
let mut buf = [0u8; 4];
reader.read(&mut buf).unwrap();
assert_eq!(buf, [1, 2, 3, 4]);
}
#[test]
fn memory_source_replace() {
let mut src = MemoryAssetSource::new();
src.insert("a.raw", vec![1]);
src.insert("a.raw", vec![2, 3]);
let mut reader = src.open("a.raw").unwrap();
assert_eq!(reader.len(), 2);
let mut buf = [0u8; 2];
reader.read(&mut buf).unwrap();
assert_eq!(buf, [2, 3]);
}
fn make_desc(marker: u8) -> ImageDescriptor<'static> {
ImageDescriptor {
format: PixelFormat::Rgb565,
width: 1,
height: 1,
data: ImageData::Owned(alloc::vec![marker]),
stride: None,
}
}
#[test]
fn slot_cache_basic_insert_and_get() {
let mut cache: SlotCache<4> = SlotCache::new();
let h = cache.insert(make_desc(42));
let desc = cache.get(h).unwrap();
assert_eq!(desc.data.as_bytes().unwrap(), &[42u8]);
}
#[test]
fn slot_cache_evicts_lru() {
let mut cache: SlotCache<2> = SlotCache::new();
let h1 = cache.insert(make_desc(1)); let h2 = cache.insert(make_desc(2)); let _ = cache.get(h1); let h3 = cache.insert(make_desc(3));
assert!(cache.get(h2).is_none(), "h2 should have been evicted");
assert!(cache.get(h1).is_some(), "h1 should still be cached");
assert!(cache.get(h3).is_some(), "h3 should be cached");
}
#[test]
fn slot_cache_evict_explicit() {
let mut cache: SlotCache<2> = SlotCache::new();
let h = cache.insert(make_desc(7));
assert!(cache.get(h).is_some());
cache.evict(h);
assert!(cache.get(h).is_none());
}
#[test]
fn slot_cache_touch_updates_recency() {
let mut cache: SlotCache<2> = SlotCache::new();
let h1 = cache.insert(make_desc(1));
let h2 = cache.insert(make_desc(2));
let _ = cache.get(h1);
let h3 = cache.insert(make_desc(3));
assert!(cache.get(h2).is_none(), "h2 evicted after h1 was touched");
assert!(cache.get(h1).is_some());
assert!(cache.get(h3).is_some());
}
#[test]
fn registry_register_and_resolve_embedded() {
let mut reg: AssetRegistry<4> = AssetRegistry::new();
reg.register_source(
&AssetPath::Embedded(""),
Box::new(EmbeddedAssetSource::new(EMBED_TABLE)),
)
.unwrap();
let handle = reg.register(AssetPath::Embedded("icons/red_green.raw"));
let desc = reg.resolve_image(handle).unwrap();
assert!(!desc.data.is_empty());
}
#[test]
fn registry_resolve_unknown_handle_errors() {
let mut reg: AssetRegistry<4> = AssetRegistry::new();
let fake_handle = AssetHandle(99);
let err = reg.resolve_image(fake_handle).unwrap_err();
assert!(matches!(err, AssetError::Fs(FsError::NoSuchFile)));
}
#[test]
fn registry_resolve_cache_hit_on_second_call() {
let mut reg: AssetRegistry<4> = AssetRegistry::new();
reg.register_source(
&AssetPath::Embedded(""),
Box::new(EmbeddedAssetSource::new(EMBED_TABLE)),
)
.unwrap();
let handle = reg.register(AssetPath::Embedded("icons/red_green.raw"));
let _ = reg.resolve_image(handle).unwrap();
let desc = reg.resolve_image(handle).unwrap();
assert!(!desc.data.is_empty());
}
#[test]
fn registry_no_source_for_kind_errors() {
let mut reg: AssetRegistry<4> = AssetRegistry::new();
reg.register_source(
&AssetPath::Memory(String::new()),
Box::new(MemoryAssetSource::new()),
)
.unwrap();
let handle = reg.register(AssetPath::Embedded("foo.raw"));
let err = reg.resolve_image(handle).unwrap_err();
assert!(matches!(err, AssetError::Fs(FsError::NoSuchFile)));
}
#[test]
fn registry_memory_source_round_trip() {
let mut src = MemoryAssetSource::new();
src.insert("logo.raw", vec![0xAA, 0xBB, 0xCC, 0xDD]);
let mut reg: AssetRegistry<4> = AssetRegistry::new();
reg.register_source(&AssetPath::Memory(String::new()), Box::new(src))
.unwrap();
let handle = reg.register(AssetPath::Memory("logo.raw".into()));
let desc = reg.resolve_image(handle).unwrap();
let bytes = desc.data.as_bytes().unwrap();
assert_eq!(bytes, &[0xAA, 0xBB, 0xCC, 0xDD]);
}
#[test]
fn registry_evict_then_reload() {
let mut src = MemoryAssetSource::new();
src.insert("img.raw", vec![0x01, 0x02]);
let mut reg: AssetRegistry<1> = AssetRegistry::new(); reg.register_source(&AssetPath::Memory(String::new()), Box::new(src))
.unwrap();
let h1 = reg.register(AssetPath::Memory("img.raw".into()));
let h2 = reg.register(AssetPath::Memory("img.raw".into()));
let _ = reg.resolve_image(h1).unwrap();
let _ = reg.resolve_image(h2).unwrap();
let desc = reg.resolve_image(h1).unwrap();
assert!(!desc.data.is_empty());
}
#[test]
fn image_data_asset_variant_constructs_and_matches() {
let handle = AssetHandle(1);
let data = ImageData::Asset(handle);
assert_eq!(data.byte_len(), 0);
assert!(data.is_empty());
assert!(data.as_bytes().is_none());
assert!(data.as_color_slice().is_none());
match &data {
ImageData::Asset(h) => assert_eq!(h.as_u32(), 1),
_ => panic!("expected Asset variant"),
}
}
#[test]
fn existing_image_data_variants_unaffected() {
let borrowed_data = ImageData::Borrowed(&[1u8, 2, 3]);
assert_eq!(borrowed_data.byte_len(), 3);
assert!(borrowed_data.as_bytes().is_some());
let owned_data: ImageData<'_> = ImageData::Owned(vec![4, 5]);
assert_eq!(owned_data.byte_len(), 2);
let check = match owned_data {
ImageData::Borrowed(_) => "borrowed",
ImageData::BorrowedColors(_) => "colors",
ImageData::Owned(_) => "owned",
ImageData::Asset(_) => "asset",
};
assert_eq!(check, "owned");
}
#[test]
fn asset_path_helpers() {
let p = AssetPath::Embedded("icons/ok.raw");
assert_eq!(p.path_str(), "icons/ok.raw");
assert_eq!(p.source_kind(), "embedded");
let p2 = AssetPath::Memory("fonts/mono.bin".into());
assert_eq!(p2.source_kind(), "memory");
assert_eq!(p2.path_str(), "fonts/mono.bin");
}
#[cfg(all(feature = "sim", not(target_os = "none")))]
#[test]
fn sim_source_reads_real_file() {
use std::io::Write as _;
let dir = std::env::temp_dir();
let path = dir.join("rlvgl_sim_test.raw");
{
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&[0xDE, 0xAD]).unwrap();
}
let src = SimAssetSource::new(dir);
let mut reader = src.open("rlvgl_sim_test.raw").unwrap();
let mut buf = [0u8; 2];
reader.read(&mut buf).unwrap();
assert_eq!(buf, [0xDE, 0xAD]);
std::fs::remove_file(path).ok();
}
}