use std::time::Duration;
use crate::CursorError;
use crate::theme::{Theme, ThemeResolution};
use crate::xcursor::{self, Limits};
const DEFAULT_SIZE: u32 = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers, with no default to fill a new field from"
)]
pub struct Frame {
pub pixels: Vec<u32>,
pub width: u32,
pub height: u32,
pub xhot: u32,
pub yhot: u32,
pub delay: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor {
frames: Vec<Frame>,
cycle: Duration,
}
impl Cursor {
pub fn load(resolved: &ThemeResolution, requested_size: u32) -> Result<Self, CursorError> {
let bytes = std::fs::read(&resolved.source).map_err(|e| CursorError::Io(e.to_string()))?;
Self::from_bytes(&bytes, requested_size)
}
pub fn load_named(
theme: &Theme,
cursor_name: &str,
preferred_theme: &str,
requested_size: u32,
) -> Result<Self, CursorError> {
let resolved = theme.resolve(cursor_name, preferred_theme)?;
Self::load(&resolved, requested_size)
}
pub fn from_bytes(bytes: &[u8], requested_size: u32) -> Result<Self, CursorError> {
let size = if requested_size == 0 {
DEFAULT_SIZE
} else {
requested_size
};
let images = xcursor::parse(bytes, Limits::default())?;
let chosen = xcursor::nearest_size(&images, size).ok_or(CursorError::NoImages)?;
let frames: Vec<Frame> = images
.into_iter()
.filter(|image| image.nominal_size == chosen)
.map(|image| Frame {
pixels: image.pixels,
width: image.width,
height: image.height,
xhot: image.xhot,
yhot: image.yhot,
delay: Duration::from_millis(u64::from(image.delay_ms)),
})
.collect();
Self::assemble(frames)
}
pub fn from_argb(
pixels: &[u32],
width: u32,
height: u32,
xhot: u32,
yhot: u32,
) -> Result<Self, CursorError> {
let limits = Limits::default();
if width == 0 || height == 0 || width > limits.max_dim || height > limits.max_dim {
return Err(CursorError::InvalidImage);
}
if pixels.len() != width as usize * height as usize {
return Err(CursorError::InvalidImage);
}
Self::assemble(vec![Frame {
pixels: pixels.to_vec(),
width,
height,
xhot,
yhot,
delay: Duration::ZERO,
}])
}
pub fn from_frames(frames: Vec<Frame>) -> Result<Self, CursorError> {
let limits = Limits::default();
for frame in &frames {
if frame.width == 0
|| frame.height == 0
|| frame.width > limits.max_dim
|| frame.height > limits.max_dim
{
return Err(CursorError::InvalidImage);
}
if frame.pixels.len() != frame.width as usize * frame.height as usize {
return Err(CursorError::InvalidImage);
}
}
if frames.len() > limits.max_frames {
return Err(CursorError::TooLarge);
}
Self::assemble(frames)
}
fn assemble(frames: Vec<Frame>) -> Result<Self, CursorError> {
if frames.is_empty() {
return Err(CursorError::NoImages);
}
let cycle = frames.iter().map(|frame| frame.delay).sum();
Ok(Self { frames, cycle })
}
#[must_use]
pub fn first(&self) -> &Frame {
&self.frames[0]
}
#[must_use]
pub fn frames(&self) -> &[Frame] {
&self.frames
}
#[must_use]
pub const fn cycle(&self) -> Duration {
self.cycle
}
#[must_use]
pub fn is_animated(&self) -> bool {
self.frames.len() > 1 && !self.cycle.is_zero()
}
#[must_use]
pub fn frame_at(&self, elapsed: Duration) -> &Frame {
let index = self.frame_index_at(elapsed);
self.frames.get(index).unwrap_or_else(|| self.first())
}
#[must_use]
pub fn frame_index_at(&self, elapsed: Duration) -> usize {
if !self.is_animated() {
return 0;
}
let into_cycle = Duration::from_nanos(
u64::try_from(elapsed.as_nanos() % self.cycle.as_nanos()).unwrap_or(0),
);
let mut accumulated = Duration::ZERO;
for (index, frame) in self.frames.iter().enumerate() {
accumulated += frame.delay;
if into_cycle < accumulated {
return index;
}
}
self.frames.len().saturating_sub(1)
}
}