use denise::{Color, Point, Rect, Role, Theme};
use denise_render::Canvas;
#[derive(Clone, Copy, Debug)]
pub struct CursorImage {
pub width: i32,
pub height: i32,
pub hotspot: Point,
pub mask: &'static [u8],
}
impl CursorImage {
#[inline]
pub const fn is_well_formed(&self) -> bool {
self.width > 0 && self.height > 0 && self.mask.len() == (self.width * self.height) as usize
}
#[inline]
pub fn bounds_at(&self, at: Point) -> Rect {
Rect::new(
at.x - self.hotspot.x,
at.y - self.hotspot.y,
self.width,
self.height,
)
}
}
pub const ARROW: CursorImage = CursorImage {
width: 12,
height: 18,
hotspot: Point::new(0, 0),
mask: concat!(
"+...........",
"++..........",
"+#+.........",
"+##+........",
"+###+.......",
"+####+......",
"+#####+.....",
"+######+....",
"+#######+...",
"+########+..",
"+#####+++++.",
"+##+##+.....",
"+#+.+##+....",
"++..+##+....",
".....+##+...",
".....+##+...",
"......+#+...",
"......+++...",
)
.as_bytes(),
};
pub const CROSSHAIR: CursorImage = CursorImage {
width: 15,
height: 15,
hotspot: Point::new(7, 7),
mask: concat!(
"......+#+......",
"......+#+......",
"......+#+......",
"......+#+......",
"......+#+......",
"......+++......",
"+++++.....+++++",
"#####..#..#####",
"+++++.....+++++",
"......+++......",
"......+#+......",
"......+#+......",
"......+#+......",
"......+#+......",
"......+#+......",
)
.as_bytes(),
};
#[derive(Clone, Copy, Debug)]
pub struct Cursor {
pub image: &'static CursorImage,
pub position: Point,
pub visible: bool,
}
impl Default for Cursor {
fn default() -> Self {
Self {
image: &ARROW,
position: Point::ZERO,
visible: false,
}
}
}
impl Cursor {
#[inline]
pub fn bounds(&self) -> Rect {
if self.visible {
self.image.bounds_at(self.position)
} else {
Rect::ZERO
}
}
pub fn paint(&self, theme: &Theme, canvas: &mut Canvas<'_>) {
if !self.visible || !self.image.is_well_formed() {
return;
}
let origin = self.image.bounds_at(self.position);
if canvas.visible(origin).is_none() {
return;
}
let fill = theme.color(Role::BaseContent);
let outline = theme.color(Role::Base100);
paint_mask(self.image, origin, fill, outline, canvas);
}
}
fn paint_mask(
image: &CursorImage,
origin: Rect,
fill: Color,
outline: Color,
canvas: &mut Canvas<'_>,
) {
for row in 0..image.height {
let base = (row * image.width) as usize;
let y = origin.y + row;
let mut x = 0;
while x < image.width {
let value = image.mask[base + x as usize];
let mut end = x + 1;
while end < image.width && image.mask[base + end as usize] == value {
end += 1;
}
let color = match value {
b'#' => Some(fill),
b'+' => Some(outline),
_ => None,
};
if let Some(color) = color {
canvas.fill_rect(Rect::new(origin.x + x, y, end - x, 1), color);
}
x = end;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use denise::{PixelFormat, Size, theme};
#[test]
fn built_in_sprites_match_their_declared_geometry() {
assert!(ARROW.is_well_formed(), "arrow mask is the wrong length");
assert!(
CROSSHAIR.is_well_formed(),
"crosshair mask is the wrong length"
);
}
#[test]
fn the_hotspot_pixel_is_opaque() {
for image in [&ARROW, &CROSSHAIR] {
let i = (image.hotspot.y * image.width + image.hotspot.x) as usize;
assert_ne!(
image.mask[i], b'.',
"the pixel under the pointer position must be drawn"
);
}
}
#[test]
fn a_hidden_cursor_paints_nothing() {
let mut pixels = [0u32; 64 * 64];
let mut canvas =
Canvas::from_pixels(&mut pixels, Size::new(64, 64), 64, PixelFormat::Xrgb8888)
.expect("canvas");
let cursor = Cursor::default();
cursor.paint(&theme::DARK, &mut canvas);
assert!(pixels.iter().all(|&p| p == 0));
}
#[test]
fn the_sprite_stays_inside_its_own_bounds() {
let mut pixels = [0u32; 64 * 64];
let cursor = Cursor {
image: &ARROW,
position: Point::new(20, 20),
visible: true,
};
{
let mut canvas =
Canvas::from_pixels(&mut pixels, Size::new(64, 64), 64, PixelFormat::Xrgb8888)
.expect("canvas");
cursor.paint(&theme::DARK, &mut canvas);
}
let bounds = cursor.bounds();
for y in 0..64i32 {
for x in 0..64i32 {
if !bounds.contains(Point::new(x, y)) {
assert_eq!(pixels[(y * 64 + x) as usize], 0, "wrote outside at {x},{y}");
}
}
}
assert_ne!(
pixels[(20 * 64 + 20) as usize],
0,
"the tip should be drawn"
);
}
}