use std::fmt;
use anyhow::{Context, Result, bail};
use fast_image_resize::{
PixelType, ResizeAlg, ResizeOptions, Resizer,
images::{Image, ImageRef},
};
use ico::{IconDirEntry, ResourceType};
#[derive(Clone)]
pub struct CursorImage {
width: u32,
height: u32,
hotspot_x: u32,
hotspot_y: u32,
rgba: Vec<u8>,
delay: u32,
}
impl CursorImage {
pub fn new(
width: u32,
height: u32,
hotspot_x: u32,
hotspot_y: u32,
rgba: Vec<u8>,
delay: u32,
) -> Result<Self> {
if width == 0 {
bail!("width cannot be zero");
}
if height == 0 {
bail!("height cannot be zero")
}
if hotspot_x > width {
bail!("hotspot_x={hotspot_x} cannot be greater than width={width}");
}
if hotspot_y > height {
bail!("hotspot_y={hotspot_y} cannot be greater than height={height}");
}
if (width * height * 4) != rgba.len().try_into()? {
bail!(
"Expected rgba.len()={}, instead got rgba.len()={}",
width * height * 4,
rgba.len()
);
}
Ok(Self {
width,
height,
hotspot_x,
hotspot_y,
rgba,
delay,
})
}
pub fn from_entry(entry: &IconDirEntry, delay: u32) -> Result<Self> {
if entry.resource_type() == ResourceType::Icon {
bail!(
"can't create CursorImage with resource_type={:?}",
ResourceType::Icon
);
}
let (hotspot_x, hotspot_y) = entry
.cursor_hotspot()
.context("failed to extract hotspot to construct CursorImage")?;
let rgba = entry
.decode()
.context("failed to decode RGBA to construct CursorImage")?
.into_rgba_data();
Self::new(
entry.width(),
entry.height(),
hotspot_x.into(),
hotspot_y.into(),
rgba,
delay,
)
}
pub fn scaled_to(&self, scale_factor: f64, algorithm: ResizeAlg) -> Result<Self> {
let (w1, h1) = self.dimensions();
let (w2, h2) = Self::scale_point((w1, h1), scale_factor);
let (hx2, hy2) = Self::scale_point(self.hotspot(), scale_factor);
let src = ImageRef::new(w1, h1, self.rgba(), PixelType::U8x4)?;
let mut dst = Image::new(w2, h2, PixelType::U8x4);
let mut resizer = Resizer::new();
let options = ResizeOptions::new().resize_alg(algorithm);
resizer.resize(&src, &mut dst, &options)?;
Self::new(w2, h2, hx2, hy2, dst.into_vec(), self.delay)
}
#[must_use]
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn scale_point(point: (u32, u32), scale_factor: f64) -> (u32, u32) {
(
(f64::from(point.0) * scale_factor).floor() as u32,
(f64::from(point.1) * scale_factor).floor() as u32,
)
}
#[must_use]
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[must_use]
pub const fn hotspot(&self) -> (u32, u32) {
(self.hotspot_x, self.hotspot_y)
}
#[must_use]
pub const fn delay(&self) -> u32 {
self.delay
}
#[must_use]
pub fn rgba(&self) -> &[u8] {
&self.rgba
}
#[must_use]
pub fn nominal_size(&self) -> u32 {
self.dimensions().0.max(self.dimensions().1)
}
}
impl fmt::Debug for CursorImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CursorImage")
.field("width", &self.width)
.field("height", &self.height)
.field("hotspot_x", &self.hotspot_x)
.field("hotspot_y", &self.hotspot_y)
.field("delay", &self.delay)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
#[expect(clippy::len_without_is_empty)] pub struct CursorImages {
inner: Vec<CursorImage>,
}
impl CursorImages {
#[must_use]
pub fn first(&self) -> &CursorImage {
&self.inner[0]
}
#[must_use]
pub const fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn inner(&self) -> &[CursorImage] {
&self.inner
}
}
impl TryFrom<Vec<CursorImage>> for CursorImages {
type Error = anyhow::Error;
fn try_from(vec: Vec<CursorImage>) -> Result<Self> {
if vec.is_empty() {
bail!("can't create CursorImages from empty vec");
}
if vec.len() == 1 {
let mut vec = vec;
if vec[0].delay != 0 {
vec[0].delay = 0;
}
return Ok(Self { inner: vec });
}
let expected_dims = vec[0].dimensions();
for img in &vec {
if img.dimensions() != expected_dims {
bail!("can't create CursorImages with inconsistent image dimensions");
}
if img.delay == 0 {
bail!("animated cursors can't have frames with zero delay");
}
}
Ok(Self { inner: vec })
}
}
impl From<CursorImages> for Vec<CursorImage> {
fn from(images: CursorImages) -> Self {
images.inner
}
}
#[cfg(test)]
pub mod tests {
use super::CursorImage;
use std::sync::LazyLock;
pub static BLACK: LazyLock<CursorImage> = LazyLock::new(|| CursorImage {
width: 32,
height: 32,
hotspot_x: 0,
hotspot_y: 0,
delay: 100,
rgba: vec![0u8; 4096],
});
pub static WHITE: LazyLock<CursorImage> = LazyLock::new(|| CursorImage {
width: 32,
height: 32,
hotspot_x: 0,
hotspot_y: 0,
delay: 100,
rgba: vec![255u8; 4096],
});
}