use crate::ffi;
use crate::ffi::Color;
use crate::rgui::scratch::scratch_txt;
use std::ffi::CStr;
pub const RAYGUI_ICON_MAX_ICONS: usize = 256;
pub const RAYGUI_ICON_DATA_ELEMENTS: usize = 8;
#[cfg(feature = "raygui")]
pub(crate) fn validate_rgi_header(
buf: &[u8],
) -> Result<(u16, u16), crate::core::error::LoadIconsError> {
use crate::core::error::LoadIconsError;
if buf.len() < 12 {
return Err(LoadIconsError::HeaderTruncated(buf.len()));
}
let sig: [u8; 4] = buf[..4].try_into().expect("sliced to 4");
if &sig != b"rGI " {
return Err(LoadIconsError::InvalidSignature(sig));
}
let icon_count = u16::from_le_bytes(buf[8..10].try_into().expect("sliced to 2"));
let icon_size = u16::from_le_bytes(buf[10..12].try_into().expect("sliced to 2"));
if icon_size != 16 {
return Err(LoadIconsError::UnsupportedIconSize {
expected: 16,
actual: icon_size,
});
}
if (icon_count as usize) > RAYGUI_ICON_MAX_ICONS {
return Err(LoadIconsError::TooManyIcons {
max: RAYGUI_ICON_MAX_ICONS as u16,
actual: icon_count,
});
}
Ok((icon_count, icon_size))
}
#[cfg(feature = "raygui")]
pub(crate) fn check_i32_len(len: usize) -> Result<i32, crate::core::error::LoadIconsError> {
i32::try_from(len).map_err(|_| crate::core::error::LoadIconsError::LengthOverflow(len))
}
#[cfg(feature = "raygui")]
unsafe fn copy_and_free_names(
ptr: *mut *mut std::os::raw::c_char,
icon_count: usize,
) -> Vec<String> {
if ptr.is_null() {
return Vec::new();
}
let mut names = Vec::with_capacity(icon_count);
for i in 0..icon_count {
let cstr_ptr = unsafe { *ptr.add(i) };
if cstr_ptr.is_null() {
names.push(String::new());
} else {
let s = unsafe { CStr::from_ptr(cstr_ptr) }
.to_string_lossy()
.into_owned();
names.push(s);
unsafe { ffi::MemFree(cstr_ptr.cast()) };
}
}
unsafe { ffi::MemFree(ptr.cast()) };
names
}
#[cfg(feature = "raygui")]
fn read_header_bytes(
path: &std::path::Path,
) -> Result<Vec<u8>, crate::core::error::LoadIconsError> {
use std::io::Read;
let file = std::fs::File::open(path)?;
let mut buf = Vec::with_capacity(12);
file.take(12).read_to_end(&mut buf)?;
Ok(buf)
}
#[cfg(feature = "raygui")]
fn path_to_c_string(
path: &std::path::Path,
) -> Result<std::ffi::CString, crate::core::error::LoadIconsError> {
std::ffi::CString::new(path.to_string_lossy().as_bytes()).map_err(|_| {
crate::core::error::LoadIconsError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"path contains an interior NUL byte",
))
})
}
pub trait RaylibGuiIcons {
#[inline]
fn gui_icon_text(
&mut self,
icon_id: crate::consts::GuiIconName,
text: impl AsRef<str>,
) -> String {
let buffer = unsafe { ffi::GuiIconText(icon_id as i32, scratch_txt(text)) };
if buffer.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(buffer) }
.to_string_lossy()
.into_owned()
}
#[inline]
fn gui_set_icon_scale(&mut self, scale: i32) {
unsafe { ffi::GuiSetIconScale(scale) }
}
#[inline]
fn gui_draw_icon(
&mut self,
icon_id: crate::consts::GuiIconName,
pos_x: i32,
pos_y: i32,
pixel_size: i32,
color: impl Into<Color>,
) {
unsafe { ffi::GuiDrawIcon(icon_id as i32, pos_x, pos_y, pixel_size, color.into()) }
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_get_icons(&self) -> &[[u32; RAYGUI_ICON_DATA_ELEMENTS]; RAYGUI_ICON_MAX_ICONS] {
unsafe {
let ptr = ffi::GuiGetIcons() as *const [u32; RAYGUI_ICON_DATA_ELEMENTS];
&*(ptr as *const [[u32; RAYGUI_ICON_DATA_ELEMENTS]; RAYGUI_ICON_MAX_ICONS])
}
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_get_icons_mut(
&mut self,
) -> &mut [[u32; RAYGUI_ICON_DATA_ELEMENTS]; RAYGUI_ICON_MAX_ICONS] {
unsafe {
let ptr = ffi::GuiGetIcons() as *mut [u32; RAYGUI_ICON_DATA_ELEMENTS];
&mut *(ptr as *mut [[u32; RAYGUI_ICON_DATA_ELEMENTS]; RAYGUI_ICON_MAX_ICONS])
}
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_load_icons_from_memory(
&mut self,
data: &[u8],
) -> Result<(), crate::core::error::LoadIconsError> {
let _hdr = validate_rgi_header(data)?;
let len = check_i32_len(data.len())?;
unsafe {
let _ = ffi::GuiLoadIconsFromMemory(data.as_ptr(), len, false);
}
Ok(())
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_load_icons_from_memory_with_names(
&mut self,
data: &[u8],
) -> Result<Vec<String>, crate::core::error::LoadIconsError> {
let (icon_count, _icon_size) = validate_rgi_header(data)?;
let len = check_i32_len(data.len())?;
let ptr = unsafe { ffi::GuiLoadIconsFromMemory(data.as_ptr(), len, true) };
let names = unsafe { copy_and_free_names(ptr, icon_count as usize) };
Ok(names)
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_load_icons(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<(), crate::core::error::LoadIconsError> {
use crate::core::error::LoadIconsError;
let path = path.as_ref();
match std::fs::metadata(path) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(LoadIconsError::FileNotFound(path.to_path_buf()));
}
Err(e) => return Err(LoadIconsError::Io(e)),
}
let header = read_header_bytes(path)?;
let _hdr = validate_rgi_header(&header)?;
let c_path = path_to_c_string(path)?;
unsafe {
let _ = ffi::GuiLoadIcons(c_path.as_ptr(), false);
}
Ok(())
}
#[cfg(feature = "raygui")]
#[inline]
fn gui_load_icons_with_names(
&mut self,
path: impl AsRef<std::path::Path>,
) -> Result<Vec<String>, crate::core::error::LoadIconsError> {
use crate::core::error::LoadIconsError;
let path = path.as_ref();
match std::fs::metadata(path) {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(LoadIconsError::FileNotFound(path.to_path_buf()));
}
Err(e) => return Err(LoadIconsError::Io(e)),
}
let header = read_header_bytes(path)?;
let (icon_count, _icon_size) = validate_rgi_header(&header)?;
let c_path = path_to_c_string(path)?;
let ptr = unsafe { ffi::GuiLoadIcons(c_path.as_ptr(), true) };
let names = unsafe { copy_and_free_names(ptr, icon_count as usize) };
Ok(names)
}
}
#[cfg(all(test, feature = "software_renderer", feature = "raygui"))]
mod tests {
use super::*;
use crate::test_harness::with_headless;
#[test]
fn icons_buffer_round_trip() {
with_headless(64, 64, |rl, _thread| {
const SLOT: usize = 240;
let pattern: [u32; 8] = [0xDEADBEEF; 8];
{
let icons = rl.gui_get_icons_mut();
icons[SLOT] = pattern;
}
let icons = rl.gui_get_icons();
assert_eq!(
icons[SLOT], pattern,
"icon buffer aliases raygui's live state"
);
assert_eq!(icons.len(), 256, "RAYGUI_ICON_MAX_ICONS = 256");
});
}
#[test]
fn load_icons_from_memory_rejects_short_header() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let err = rl.gui_load_icons_from_memory(&[0u8; 7]).unwrap_err();
assert!(
matches!(err, LoadIconsError::HeaderTruncated(7)),
"got {err:?}"
);
});
}
#[test]
fn load_icons_from_memory_rejects_bad_signature() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let mut buf = [0u8; 12];
buf[..4].copy_from_slice(b"XXXX");
let err = rl.gui_load_icons_from_memory(&buf).unwrap_err();
assert!(
matches!(err, LoadIconsError::InvalidSignature(sig) if &sig == b"XXXX"),
"got {err:?}"
);
});
}
#[test]
fn load_icons_from_memory_rejects_bad_icon_size() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let mut buf = [0u8; 12];
buf[..4].copy_from_slice(b"rGI ");
buf[8..10].copy_from_slice(&1u16.to_le_bytes()); buf[10..12].copy_from_slice(&32u16.to_le_bytes()); let err = rl.gui_load_icons_from_memory(&buf).unwrap_err();
assert!(
matches!(
err,
LoadIconsError::UnsupportedIconSize {
expected: 16,
actual: 32
}
),
"got {err:?}"
);
});
}
#[test]
fn load_icons_from_memory_rejects_too_many_icons() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let mut buf = [0u8; 12];
buf[..4].copy_from_slice(b"rGI ");
buf[8..10].copy_from_slice(&300u16.to_le_bytes()); buf[10..12].copy_from_slice(&16u16.to_le_bytes()); let err = rl.gui_load_icons_from_memory(&buf).unwrap_err();
assert!(
matches!(
err,
LoadIconsError::TooManyIcons {
max: 256,
actual: 300
}
),
"got {err:?}"
);
});
}
#[test]
fn load_icons_file_not_found() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let err = rl.gui_load_icons("definitely-nonexistent.rgi").unwrap_err();
assert!(
matches!(err, LoadIconsError::FileNotFound(_)),
"got {err:?}"
);
});
}
#[test]
fn load_icons_file_bad_signature() {
use crate::core::error::LoadIconsError;
with_headless(64, 64, |rl, _thread| {
let tmp = std::env::temp_dir()
.join(format!("raylib-rs-test-bad-sig-{}.rgi", std::process::id()));
std::fs::write(&tmp, b"NOPE_NOT_AN_RGI_FILE_AT_ALL").unwrap();
let err = rl.gui_load_icons(&tmp).unwrap_err();
assert!(
matches!(err, LoadIconsError::InvalidSignature(_)),
"got {err:?}"
);
let _ = std::fs::remove_file(&tmp);
});
}
#[test]
fn load_icons_from_memory_with_names_handles_sub_256_icon_count() {
with_headless(64, 64, |rl, _thread| {
const ICON_COUNT: u16 = 1;
const NAME_LEN: usize = 32; const WORDS_PER_ICON: usize = 8;
let mut buf =
Vec::with_capacity(12 + ICON_COUNT as usize * (NAME_LEN + 4 * WORDS_PER_ICON));
buf.extend_from_slice(b"rGI ");
buf.extend_from_slice(&100u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&ICON_COUNT.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes()); let mut name = [0u8; NAME_LEN];
name[..4].copy_from_slice(b"save");
buf.extend_from_slice(&name);
for _ in 0..WORDS_PER_ICON {
buf.extend_from_slice(&0xAAAA_AAAAu32.to_le_bytes());
}
let names = rl
.gui_load_icons_from_memory_with_names(&buf)
.expect("well-formed 1-icon payload");
assert_eq!(names.len(), 1, "exactly iconCount names returned");
assert_eq!(names[0], "save", "name round-trips through raygui + Rust");
});
}
}
#[cfg(all(test, feature = "raygui"))]
mod unit_tests {
use super::*;
#[test]
fn check_i32_len_boundary() {
assert!(check_i32_len(0).is_ok());
assert!(check_i32_len(i32::MAX as usize).is_ok());
assert!(matches!(
check_i32_len(i32::MAX as usize + 1),
Err(crate::core::error::LoadIconsError::LengthOverflow(_))
));
}
#[test]
fn validate_rgi_header_accepts_well_formed() {
let mut buf = [0u8; 12];
buf[..4].copy_from_slice(b"rGI ");
buf[8..10].copy_from_slice(&5u16.to_le_bytes()); buf[10..12].copy_from_slice(&16u16.to_le_bytes()); let (count, size) = validate_rgi_header(&buf).expect("well-formed header");
assert_eq!(count, 5);
assert_eq!(size, 16);
}
}