1extern crate alloc;
36
37use alloc::borrow::ToOwned;
38use alloc::boxed::Box;
39use alloc::string::String;
40use alloc::vec::Vec;
41
42use crate::fs::{AssetError, AssetRead, AssetSource, FsError};
43use crate::image::{CacheHandle, ImageDescriptor, PixelFormat};
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub enum AssetPath {
65 Embedded(&'static str),
71 Fatfs(String),
77 Sim(String),
82 Memory(String),
87}
88
89impl AssetPath {
90 pub fn path_str(&self) -> &str {
95 match self {
96 AssetPath::Embedded(s) => s,
97 AssetPath::Fatfs(s) | AssetPath::Sim(s) | AssetPath::Memory(s) => s.as_str(),
98 }
99 }
100
101 pub fn source_kind(&self) -> &'static str {
103 match self {
104 AssetPath::Embedded(_) => "embedded",
105 AssetPath::Fatfs(_) => "fatfs",
106 AssetPath::Sim(_) => "sim",
107 AssetPath::Memory(_) => "memory",
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub struct AssetHandle(u32);
126
127impl AssetHandle {
128 pub const fn as_u32(self) -> u32 {
130 self.0
131 }
132}
133
134struct CacheEntry {
138 handle: CacheHandle,
140 ts: u32,
142 desc: ImageDescriptor<'static>,
144}
145
146pub struct SlotCache<const N: usize> {
171 slots: [Option<CacheEntry>; N],
172 ts_counter: u32,
175 handle_counter: u32,
177}
178
179impl<const N: usize> SlotCache<N> {
180 pub const fn new() -> Self {
182 #[allow(clippy::declare_interior_mutable_const)]
185 const NONE_ENTRY: Option<CacheEntry> = None;
186 Self {
187 slots: [NONE_ENTRY; N],
188 ts_counter: 1,
189 handle_counter: 1,
190 }
191 }
192
193 pub fn get(&mut self, handle: CacheHandle) -> Option<&ImageDescriptor<'static>> {
198 let ts = self.ts_counter;
199 self.ts_counter = self.ts_counter.wrapping_add(1);
200 for entry in self.slots.iter_mut().flatten() {
201 if entry.handle == handle {
202 entry.ts = ts;
203 break;
205 }
206 }
207 for entry in self.slots.iter().flatten() {
209 if entry.handle == handle {
210 return Some(&entry.desc);
211 }
212 }
213 None
214 }
215
216 pub fn insert(&mut self, descriptor: ImageDescriptor<'static>) -> CacheHandle {
221 let handle = CacheHandle::new(self.handle_counter);
222 self.handle_counter = self.handle_counter.wrapping_add(1);
223 let ts = self.ts_counter;
224 self.ts_counter = self.ts_counter.wrapping_add(1);
225
226 for slot in &mut self.slots {
228 if slot.is_none() {
229 *slot = Some(CacheEntry {
230 handle,
231 ts,
232 desc: descriptor,
233 });
234 return handle;
235 }
236 }
237
238 let evict_idx = self
240 .slots
241 .iter()
242 .enumerate()
243 .filter_map(|(i, s)| s.as_ref().map(|e| (i, e.ts)))
244 .min_by_key(|&(_, t)| t)
245 .map(|(i, _)| i)
246 .expect("N >= 1 guaranteed by construction");
247 self.slots[evict_idx] = Some(CacheEntry {
248 handle,
249 ts,
250 desc: descriptor,
251 });
252 handle
253 }
254
255 pub fn evict(&mut self, handle: CacheHandle) {
260 for slot in &mut self.slots {
261 if slot.as_ref().is_some_and(|e| e.handle == handle) {
262 *slot = None;
263 return;
264 }
265 }
266 }
267
268 pub fn len(&self) -> usize {
270 self.slots.iter().filter(|s| s.is_some()).count()
271 }
272
273 pub fn is_empty(&self) -> bool {
275 self.len() == 0
276 }
277}
278
279impl<const N: usize> Default for SlotCache<N> {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285impl<const N: usize> crate::image::ImageCache<'static> for SlotCache<N> {
286 fn get(&self, _handle: CacheHandle) -> Option<&ImageDescriptor<'static>> {
287 for entry in self.slots.iter().flatten() {
292 if entry.handle == _handle {
293 return Some(&entry.desc);
294 }
295 }
296 None
297 }
298
299 fn put(&mut self, descriptor: ImageDescriptor<'static>) -> CacheHandle {
300 self.insert(descriptor)
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309enum SourceKind {
310 Embedded,
311 Fatfs,
312 Sim,
313 Memory,
314}
315
316impl AssetPath {
317 fn kind(&self) -> SourceKind {
318 match self {
319 AssetPath::Embedded(_) => SourceKind::Embedded,
320 AssetPath::Fatfs(_) => SourceKind::Fatfs,
321 AssetPath::Sim(_) => SourceKind::Sim,
322 AssetPath::Memory(_) => SourceKind::Memory,
323 }
324 }
325}
326
327pub const ASSET_REGISTRY_MAX_SOURCES: usize = 4;
335
336struct RegistrySource {
339 kind: SourceKind,
340 source: Box<dyn AssetSource>,
341}
342
343struct HandleRecord {
346 asset_handle: AssetHandle,
347 path: AssetPath,
348 cache_handle: Option<CacheHandle>,
349}
350
351pub struct AssetRegistry<const CACHE_SLOTS: usize = 8> {
368 sources: Vec<RegistrySource>,
369 handles: Vec<HandleRecord>,
370 cache: SlotCache<CACHE_SLOTS>,
371 next_asset_handle: u32,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum RegistryError {
377 Full,
379 DuplicateKind,
381}
382
383impl<const CACHE_SLOTS: usize> AssetRegistry<CACHE_SLOTS> {
384 pub fn new() -> Self {
386 Self {
387 sources: Vec::new(),
388 handles: Vec::new(),
389 cache: SlotCache::new(),
390 next_asset_handle: 1,
392 }
393 }
394
395 pub fn register_source(
407 &mut self,
408 kind_sentinel: &AssetPath,
409 source: Box<dyn AssetSource>,
410 ) -> Result<(), RegistryError> {
411 if self.sources.len() >= ASSET_REGISTRY_MAX_SOURCES {
412 return Err(RegistryError::Full);
413 }
414 let kind = kind_sentinel.kind();
415 if self.sources.iter().any(|s| s.kind == kind) {
416 return Err(RegistryError::DuplicateKind);
417 }
418 self.sources.push(RegistrySource { kind, source });
419 Ok(())
420 }
421
422 pub fn register(&mut self, path: AssetPath) -> AssetHandle {
427 let handle = AssetHandle(self.next_asset_handle);
428 self.next_asset_handle = self.next_asset_handle.wrapping_add(1);
429 self.handles.push(HandleRecord {
430 asset_handle: handle,
431 path,
432 cache_handle: None,
433 });
434 handle
435 }
436
437 pub fn resolve_image(
465 &mut self,
466 handle: AssetHandle,
467 ) -> Result<&ImageDescriptor<'static>, AssetError> {
468 let record_idx = self
470 .handles
471 .iter()
472 .position(|r| r.asset_handle == handle)
473 .ok_or(AssetError::Fs(FsError::NoSuchFile))?;
474
475 let final_cache_handle: CacheHandle;
481
482 let existing_cache_handle: Option<CacheHandle> = self.handles[record_idx]
484 .cache_handle
485 .filter(|&ch| self.cache.slots.iter().flatten().any(|e| e.handle == ch));
486
487 if let Some(ch) = existing_cache_handle {
488 let _ = self.cache.get(ch);
490 final_cache_handle = ch;
491 } else {
492 self.handles[record_idx].cache_handle = None;
494
495 let path_str = self.handles[record_idx].path.path_str().to_owned();
497 let kind = self.handles[record_idx].path.kind();
498
499 let source_idx = self
500 .sources
501 .iter()
502 .position(|s| s.kind == kind)
503 .ok_or(AssetError::Fs(FsError::NoSuchFile))?;
504
505 let bytes = {
506 let source = &self.sources[source_idx].source;
507 let mut reader = source.open(&path_str)?;
508 let len = reader.len();
509 let mut buf = alloc::vec![0u8; len];
510 let mut off = 0usize;
511 while off < len {
512 let n = reader.read(&mut buf[off..])?;
513 if n == 0 {
514 break;
515 }
516 off += n;
517 }
518 buf
519 };
520
521 let desc = decode_bytes(&path_str, bytes)?;
523
524 let new_ch = self.cache.insert(desc);
526 self.handles[record_idx].cache_handle = Some(new_ch);
527 final_cache_handle = new_ch;
528 }
529
530 for e in self.cache.slots.iter().flatten() {
532 if e.handle == final_cache_handle {
533 return Ok(&e.desc);
534 }
535 }
536 Err(AssetError::Fs(FsError::Device))
538 }
539}
540
541impl<const CACHE_SLOTS: usize> Default for AssetRegistry<CACHE_SLOTS> {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547fn decode_bytes(path: &str, bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
557 let lower = path.to_ascii_lowercase();
558
559 #[cfg(all(feature = "png", not(target_os = "none")))]
561 if lower.ends_with(".png") || bytes.get(..8).is_some_and(|h| h.starts_with(b"\x89PNG")) {
562 return decode_png(bytes);
563 }
564
565 #[cfg(all(feature = "jpeg", not(target_os = "none")))]
567 if lower.ends_with(".jpg")
568 || lower.ends_with(".jpeg")
569 || bytes.get(..2).is_some_and(|h| h == b"\xff\xd8")
570 {
571 return decode_jpeg(bytes);
572 }
573
574 #[cfg(feature = "gif")]
576 if lower.ends_with(".gif") || bytes.get(..4).is_some_and(|h| h == b"GIF8") {
577 return decode_gif(bytes);
578 }
579
580 let _ = lower; Ok(ImageDescriptor {
585 format: PixelFormat::Rgb565,
586 width: 0,
587 height: 0,
588 data: crate::image::ImageData::Owned(bytes),
589 stride: None,
590 })
591}
592
593#[cfg(all(feature = "png", not(target_os = "none")))]
594fn decode_png(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
595 let (colors, w, h) = crate::plugins::png::decode(&bytes)
596 .map_err(|e| AssetError::Decode(alloc::format!("png: {e:?}")))?;
597 let mut pixels = Vec::with_capacity(colors.len() * 4);
599 for c in &colors {
600 pixels.push(c.0); pixels.push(c.1); pixels.push(c.2); pixels.push(c.3); }
605 Ok(ImageDescriptor {
606 format: PixelFormat::Argb8888,
607 width: w as u16,
608 height: h as u16,
609 data: crate::image::ImageData::Owned(pixels),
610 stride: None,
611 })
612}
613
614#[cfg(all(feature = "jpeg", not(target_os = "none")))]
615fn decode_jpeg(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
616 let (colors, w, h) = crate::plugins::jpeg::decode(&bytes)
617 .map_err(|e| AssetError::Decode(alloc::format!("jpeg: {e:?}")))?;
618 let mut pixels = Vec::with_capacity(colors.len() * 4);
619 for c in &colors {
620 pixels.push(c.0);
621 pixels.push(c.1);
622 pixels.push(c.2);
623 pixels.push(c.3);
624 }
625 Ok(ImageDescriptor {
626 format: PixelFormat::Argb8888,
627 width: w,
628 height: h,
629 data: crate::image::ImageData::Owned(pixels),
630 stride: None,
631 })
632}
633
634#[cfg(feature = "gif")]
635fn decode_gif(bytes: Vec<u8>) -> Result<ImageDescriptor<'static>, AssetError> {
636 let (frames, w, h) = crate::plugins::gif::decode(&bytes)
637 .map_err(|e| AssetError::Decode(alloc::format!("gif: {e:?}")))?;
638 let frame = frames
640 .into_iter()
641 .next()
642 .ok_or_else(|| AssetError::Decode(alloc::string::String::from("gif: no frames")))?;
643 let mut pixels = Vec::with_capacity(frame.pixels.len() * 4);
644 for c in &frame.pixels {
645 pixels.push(c.0);
646 pixels.push(c.1);
647 pixels.push(c.2);
648 pixels.push(c.3);
649 }
650 Ok(ImageDescriptor {
651 format: PixelFormat::Argb8888,
652 width: w,
653 height: h,
654 data: crate::image::ImageData::Owned(pixels),
655 stride: None,
656 })
657}
658
659struct StaticSliceReader {
663 data: &'static [u8],
664 pos: usize,
665}
666
667impl AssetRead for StaticSliceReader {
668 fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
669 let remaining = self.data.len().saturating_sub(self.pos);
670 let n = out.len().min(remaining);
671 out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
672 self.pos += n;
673 Ok(n)
674 }
675
676 fn len(&self) -> usize {
677 self.data.len()
678 }
679
680 fn is_empty(&self) -> bool {
681 self.data.is_empty()
682 }
683
684 fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
685 self.pos = (pos as usize).min(self.data.len());
686 Ok(self.pos as u64)
687 }
688}
689
690pub struct EmbeddedAssetSource {
705 table: &'static [(&'static str, &'static [u8])],
706}
707
708impl EmbeddedAssetSource {
709 pub const fn new(table: &'static [(&'static str, &'static [u8])]) -> Self {
714 Self { table }
715 }
716}
717
718impl AssetSource for EmbeddedAssetSource {
719 fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
720 for &(name, bytes) in self.table {
721 if name == path {
722 return Ok(Box::new(StaticSliceReader {
723 data: bytes,
724 pos: 0,
725 }));
726 }
727 }
728 Err(AssetError::Fs(FsError::NoSuchFile))
729 }
730
731 fn exists(&self, path: &str) -> bool {
732 self.table.iter().any(|&(name, _)| name == path)
733 }
734
735 fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
736 Ok(crate::fs::AssetIter)
737 }
738}
739
740struct VecReader {
744 data: Vec<u8>,
745 pos: usize,
746}
747
748impl AssetRead for VecReader {
749 fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
750 let remaining = self.data.len().saturating_sub(self.pos);
751 let n = out.len().min(remaining);
752 out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
753 self.pos += n;
754 Ok(n)
755 }
756
757 fn len(&self) -> usize {
758 self.data.len()
759 }
760
761 fn is_empty(&self) -> bool {
762 self.data.is_empty()
763 }
764
765 fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
766 self.pos = (pos as usize).min(self.data.len());
767 Ok(self.pos as u64)
768 }
769}
770
771pub struct MemoryAssetSource {
781 entries: Vec<(String, Vec<u8>)>,
782}
783
784impl MemoryAssetSource {
785 pub fn new() -> Self {
787 Self {
788 entries: Vec::new(),
789 }
790 }
791
792 pub fn insert(&mut self, name: impl Into<String>, data: Vec<u8>) {
796 let name = name.into();
797 for entry in &mut self.entries {
798 if entry.0 == name {
799 entry.1 = data;
800 return;
801 }
802 }
803 self.entries.push((name, data));
804 }
805}
806
807impl Default for MemoryAssetSource {
808 fn default() -> Self {
809 Self::new()
810 }
811}
812
813impl AssetSource for MemoryAssetSource {
814 fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
815 for (name, data) in &self.entries {
816 if name == path {
817 return Ok(Box::new(VecReader {
818 data: data.clone(),
819 pos: 0,
820 }));
821 }
822 }
823 Err(AssetError::Fs(FsError::NoSuchFile))
824 }
825
826 fn exists(&self, path: &str) -> bool {
827 self.entries.iter().any(|(name, _)| name == path)
828 }
829
830 fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
831 Ok(crate::fs::AssetIter)
832 }
833}
834
835#[cfg(all(feature = "sim", not(target_os = "none")))]
847pub struct SimAssetSource {
848 prefix: std::path::PathBuf,
849}
850
851#[cfg(all(feature = "sim", not(target_os = "none")))]
852impl SimAssetSource {
853 pub fn new(prefix: impl Into<std::path::PathBuf>) -> Self {
857 Self {
858 prefix: prefix.into(),
859 }
860 }
861}
862
863#[cfg(all(feature = "sim", not(target_os = "none")))]
864struct StdFileReader {
865 data: Vec<u8>,
866 pos: usize,
867}
868
869#[cfg(all(feature = "sim", not(target_os = "none")))]
870impl AssetRead for StdFileReader {
871 fn read(&mut self, out: &mut [u8]) -> Result<usize, AssetError> {
872 let remaining = self.data.len().saturating_sub(self.pos);
873 let n = out.len().min(remaining);
874 out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
875 self.pos += n;
876 Ok(n)
877 }
878
879 fn len(&self) -> usize {
880 self.data.len()
881 }
882
883 fn is_empty(&self) -> bool {
884 self.data.is_empty()
885 }
886
887 fn seek(&mut self, pos: u64) -> Result<u64, AssetError> {
888 self.pos = (pos as usize).min(self.data.len());
889 Ok(self.pos as u64)
890 }
891}
892
893#[cfg(all(feature = "sim", not(target_os = "none")))]
894impl AssetSource for SimAssetSource {
895 fn open<'a>(&'a self, path: &str) -> Result<Box<dyn AssetRead + 'a>, AssetError> {
896 use std::io::Read as _;
897 let full = self.prefix.join(path);
898 let mut file = std::fs::File::open(&full).map_err(|e| {
899 if e.kind() == std::io::ErrorKind::NotFound {
900 AssetError::Fs(FsError::NoSuchFile)
901 } else {
902 AssetError::Fs(FsError::Device)
903 }
904 })?;
905 let mut data = Vec::new();
906 file.read_to_end(&mut data)
907 .map_err(|_| AssetError::Fs(FsError::Device))?;
908 Ok(Box::new(StdFileReader { data, pos: 0 }))
909 }
910
911 fn exists(&self, path: &str) -> bool {
912 self.prefix.join(path).exists()
913 }
914
915 fn list(&self, _dir: &str) -> Result<crate::fs::AssetIter, AssetError> {
916 Ok(crate::fs::AssetIter)
917 }
918}
919
920#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::image::{ImageData, PixelFormat};
926
927 static PIXEL_2X1: &[u8] = &[0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF]; static EMBED_TABLE: &[(&str, &[u8])] = &[("icons/red_green.raw", PIXEL_2X1)];
931
932 #[test]
933 fn embedded_source_open_hit() {
934 let src = EmbeddedAssetSource::new(EMBED_TABLE);
935 assert!(src.exists("icons/red_green.raw"));
936 let mut reader = src.open("icons/red_green.raw").unwrap();
937 assert_eq!(reader.len(), 8);
938 let mut buf = [0u8; 8];
939 let n = reader.read(&mut buf).unwrap();
940 assert_eq!(n, 8);
941 assert_eq!(&buf, PIXEL_2X1);
942 }
943
944 #[test]
945 fn embedded_source_open_miss() {
946 let src = EmbeddedAssetSource::new(EMBED_TABLE);
947 assert!(!src.exists("missing.raw"));
948 let result = src.open("missing.raw");
949 assert!(matches!(result, Err(AssetError::Fs(FsError::NoSuchFile))));
950 }
951
952 #[test]
955 fn memory_source_round_trip() {
956 let mut src = MemoryAssetSource::new();
957 src.insert("test.raw", vec![1, 2, 3, 4]);
958 assert!(src.exists("test.raw"));
959 let mut reader = src.open("test.raw").unwrap();
960 let mut buf = [0u8; 4];
961 reader.read(&mut buf).unwrap();
962 assert_eq!(buf, [1, 2, 3, 4]);
963 }
964
965 #[test]
966 fn memory_source_replace() {
967 let mut src = MemoryAssetSource::new();
968 src.insert("a.raw", vec![1]);
969 src.insert("a.raw", vec![2, 3]);
970 let mut reader = src.open("a.raw").unwrap();
971 assert_eq!(reader.len(), 2);
972 let mut buf = [0u8; 2];
973 reader.read(&mut buf).unwrap();
974 assert_eq!(buf, [2, 3]);
975 }
976
977 fn make_desc(marker: u8) -> ImageDescriptor<'static> {
980 ImageDescriptor {
981 format: PixelFormat::Rgb565,
982 width: 1,
983 height: 1,
984 data: ImageData::Owned(alloc::vec![marker]),
985 stride: None,
986 }
987 }
988
989 #[test]
990 fn slot_cache_basic_insert_and_get() {
991 let mut cache: SlotCache<4> = SlotCache::new();
992 let h = cache.insert(make_desc(42));
993 let desc = cache.get(h).unwrap();
994 assert_eq!(desc.data.as_bytes().unwrap(), &[42u8]);
995 }
996
997 #[test]
998 fn slot_cache_evicts_lru() {
999 let mut cache: SlotCache<2> = SlotCache::new();
1000 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));
1006 assert!(cache.get(h2).is_none(), "h2 should have been evicted");
1007 assert!(cache.get(h1).is_some(), "h1 should still be cached");
1008 assert!(cache.get(h3).is_some(), "h3 should be cached");
1009 }
1010
1011 #[test]
1012 fn slot_cache_evict_explicit() {
1013 let mut cache: SlotCache<2> = SlotCache::new();
1014 let h = cache.insert(make_desc(7));
1015 assert!(cache.get(h).is_some());
1016 cache.evict(h);
1017 assert!(cache.get(h).is_none());
1018 }
1019
1020 #[test]
1021 fn slot_cache_touch_updates_recency() {
1022 let mut cache: SlotCache<2> = SlotCache::new();
1023 let h1 = cache.insert(make_desc(1));
1024 let h2 = cache.insert(make_desc(2));
1025 let _ = cache.get(h1);
1028 let h3 = cache.insert(make_desc(3));
1030 assert!(cache.get(h2).is_none(), "h2 evicted after h1 was touched");
1031 assert!(cache.get(h1).is_some());
1032 assert!(cache.get(h3).is_some());
1033 }
1034
1035 #[test]
1038 fn registry_register_and_resolve_embedded() {
1039 let mut reg: AssetRegistry<4> = AssetRegistry::new();
1040 reg.register_source(
1041 &AssetPath::Embedded(""),
1042 Box::new(EmbeddedAssetSource::new(EMBED_TABLE)),
1043 )
1044 .unwrap();
1045 let handle = reg.register(AssetPath::Embedded("icons/red_green.raw"));
1046 let desc = reg.resolve_image(handle).unwrap();
1047 assert!(!desc.data.is_empty());
1049 }
1050
1051 #[test]
1052 fn registry_resolve_unknown_handle_errors() {
1053 let mut reg: AssetRegistry<4> = AssetRegistry::new();
1054 let fake_handle = AssetHandle(99);
1055 let err = reg.resolve_image(fake_handle).unwrap_err();
1056 assert!(matches!(err, AssetError::Fs(FsError::NoSuchFile)));
1057 }
1058
1059 #[test]
1060 fn registry_resolve_cache_hit_on_second_call() {
1061 let mut reg: AssetRegistry<4> = AssetRegistry::new();
1062 reg.register_source(
1063 &AssetPath::Embedded(""),
1064 Box::new(EmbeddedAssetSource::new(EMBED_TABLE)),
1065 )
1066 .unwrap();
1067 let handle = reg.register(AssetPath::Embedded("icons/red_green.raw"));
1068 let _ = reg.resolve_image(handle).unwrap();
1070 let desc = reg.resolve_image(handle).unwrap();
1072 assert!(!desc.data.is_empty());
1073 }
1074
1075 #[test]
1076 fn registry_no_source_for_kind_errors() {
1077 let mut reg: AssetRegistry<4> = AssetRegistry::new();
1078 reg.register_source(
1080 &AssetPath::Memory(String::new()),
1081 Box::new(MemoryAssetSource::new()),
1082 )
1083 .unwrap();
1084 let handle = reg.register(AssetPath::Embedded("foo.raw"));
1085 let err = reg.resolve_image(handle).unwrap_err();
1086 assert!(matches!(err, AssetError::Fs(FsError::NoSuchFile)));
1087 }
1088
1089 #[test]
1090 fn registry_memory_source_round_trip() {
1091 let mut src = MemoryAssetSource::new();
1092 src.insert("logo.raw", vec![0xAA, 0xBB, 0xCC, 0xDD]);
1093
1094 let mut reg: AssetRegistry<4> = AssetRegistry::new();
1095 reg.register_source(&AssetPath::Memory(String::new()), Box::new(src))
1096 .unwrap();
1097 let handle = reg.register(AssetPath::Memory("logo.raw".into()));
1098 let desc = reg.resolve_image(handle).unwrap();
1099 let bytes = desc.data.as_bytes().unwrap();
1100 assert_eq!(bytes, &[0xAA, 0xBB, 0xCC, 0xDD]);
1101 }
1102
1103 #[test]
1104 fn registry_evict_then_reload() {
1105 let mut src = MemoryAssetSource::new();
1106 src.insert("img.raw", vec![0x01, 0x02]);
1107
1108 let mut reg: AssetRegistry<1> = AssetRegistry::new(); reg.register_source(&AssetPath::Memory(String::new()), Box::new(src))
1110 .unwrap();
1111 let h1 = reg.register(AssetPath::Memory("img.raw".into()));
1112 let h2 = reg.register(AssetPath::Memory("img.raw".into()));
1113
1114 let _ = reg.resolve_image(h1).unwrap();
1116 let _ = reg.resolve_image(h2).unwrap();
1118 let desc = reg.resolve_image(h1).unwrap();
1120 assert!(!desc.data.is_empty());
1121 }
1122
1123 #[test]
1126 fn image_data_asset_variant_constructs_and_matches() {
1127 let handle = AssetHandle(1);
1128 let data = ImageData::Asset(handle);
1129 assert_eq!(data.byte_len(), 0);
1131 assert!(data.is_empty());
1132 assert!(data.as_bytes().is_none());
1133 assert!(data.as_color_slice().is_none());
1134
1135 match &data {
1136 ImageData::Asset(h) => assert_eq!(h.as_u32(), 1),
1137 _ => panic!("expected Asset variant"),
1138 }
1139 }
1140
1141 #[test]
1142 fn existing_image_data_variants_unaffected() {
1143 let borrowed_data = ImageData::Borrowed(&[1u8, 2, 3]);
1145 assert_eq!(borrowed_data.byte_len(), 3);
1146 assert!(borrowed_data.as_bytes().is_some());
1147
1148 let owned_data: ImageData<'_> = ImageData::Owned(vec![4, 5]);
1149 assert_eq!(owned_data.byte_len(), 2);
1150
1151 let check = match owned_data {
1155 ImageData::Borrowed(_) => "borrowed",
1156 ImageData::BorrowedColors(_) => "colors",
1157 ImageData::Owned(_) => "owned",
1158 ImageData::Asset(_) => "asset",
1159 };
1160 assert_eq!(check, "owned");
1161 }
1162
1163 #[test]
1166 fn asset_path_helpers() {
1167 let p = AssetPath::Embedded("icons/ok.raw");
1168 assert_eq!(p.path_str(), "icons/ok.raw");
1169 assert_eq!(p.source_kind(), "embedded");
1170
1171 let p2 = AssetPath::Memory("fonts/mono.bin".into());
1172 assert_eq!(p2.source_kind(), "memory");
1173 assert_eq!(p2.path_str(), "fonts/mono.bin");
1174 }
1175
1176 #[cfg(all(feature = "sim", not(target_os = "none")))]
1179 #[test]
1180 fn sim_source_reads_real_file() {
1181 use std::io::Write as _;
1182 let dir = std::env::temp_dir();
1183 let path = dir.join("rlvgl_sim_test.raw");
1184 {
1185 let mut f = std::fs::File::create(&path).unwrap();
1186 f.write_all(&[0xDE, 0xAD]).unwrap();
1187 }
1188 let src = SimAssetSource::new(dir);
1189 let mut reader = src.open("rlvgl_sim_test.raw").unwrap();
1190 let mut buf = [0u8; 2];
1191 reader.read(&mut buf).unwrap();
1192 assert_eq!(buf, [0xDE, 0xAD]);
1193 std::fs::remove_file(path).ok();
1194 }
1195}