use alloc::vec::Vec;
use denise::{Rect, Size};
use denise_render::{Canvas, PixelView};
use crate::widget::{PaintCtx, Widget};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Fit {
Fill,
#[default]
Contain,
Cover,
Center,
}
#[derive(Clone, Debug)]
enum Pixels {
Owned(Vec<u32>),
Static(&'static [u32]),
}
impl Pixels {
fn as_slice(&self) -> &[u32] {
match self {
Pixels::Owned(v) => v,
Pixels::Static(s) => s,
}
}
}
#[derive(Clone, Debug)]
pub struct Image {
pixels: Pixels,
size: Size,
fit: Fit,
radius: i32,
}
impl Image {
pub fn new(pixels: Vec<u32>, size: Size) -> Self {
Self {
pixels: Pixels::Owned(pixels),
size,
fit: Fit::Contain,
radius: 0,
}
}
pub fn from_static(pixels: &'static [u32], size: Size) -> Self {
Self {
pixels: Pixels::Static(pixels),
size,
fit: Fit::Contain,
radius: 0,
}
}
pub fn with_fit(mut self, fit: Fit) -> Self {
self.fit = fit;
self
}
pub fn with_corner_radius(mut self, radius: i32) -> Self {
self.radius = radius.max(0);
self
}
#[inline]
pub const fn size(&self) -> Size {
self.size
}
pub fn set_pixels(&mut self, pixels: Vec<u32>, size: Size) {
self.pixels = Pixels::Owned(pixels);
self.size = size;
}
}
fn fit_rect(bounds: Rect, image: Size, fit: Fit) -> Rect {
let (bw, bh) = (bounds.width as i64, bounds.height as i64);
let (iw, ih) = (image.width as i64, image.height as i64);
let centred = |w: i64, h: i64| {
Rect::new(
bounds.x + ((bw - w) / 2) as i32,
bounds.y + ((bh - h) / 2) as i32,
w as i32,
h as i32,
)
};
match fit {
Fit::Fill => bounds,
Fit::Center => centred(iw, ih),
Fit::Contain | Fit::Cover => {
let width_limited = iw * bh >= ih * bw;
if width_limited == matches!(fit, Fit::Contain) {
let h = ((ih * bw + iw / 2) / iw.max(1)).max(1);
centred(bw, h)
} else {
let w = ((iw * bh + ih / 2) / ih.max(1)).max(1);
centred(w, bh)
}
}
}
}
impl Image {
pub fn is_drawable(&self) -> bool {
PixelView::new(self.pixels.as_slice(), self.size, self.size.width).is_some()
}
pub(crate) fn paint_at(&self, bounds: Rect, radius: i32, canvas: &mut Canvas<'_>) {
let Some(view) = PixelView::new(self.pixels.as_slice(), self.size, self.size.width) else {
return;
};
let dest = fit_rect(bounds, self.size, self.fit);
if dest.is_empty() {
return;
}
if radius > 0 {
let Some(shape) = dest.intersect(&bounds) else {
return;
};
canvas.blit_rounded(&view, dest, shape, radius);
} else {
canvas.blit_scaled(&view, dest);
}
}
}
impl<M: 'static> Widget<M> for Image {
fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Canvas<'_>) {
self.paint_at(ctx.bounds, self.radius, canvas);
}
}
#[cfg(test)]
mod tests {
use super::*;
const BOX: Rect = Rect::new(10, 10, 100, 50);
#[test]
fn contain_touches_the_box_on_exactly_the_limited_axis() {
let r = fit_rect(BOX, Size::new(50, 100), Fit::Contain);
assert_eq!(r.height, 50);
assert_eq!(r.width, 25);
assert_eq!(r.y, 10);
assert_eq!(r.x, 10 + (100 - 25) / 2);
let r = fit_rect(BOX, Size::new(400, 100), Fit::Contain);
assert_eq!(r.width, 100);
assert_eq!(r.height, 25);
}
#[test]
fn cover_overflows_the_box_on_exactly_the_other_axis() {
let r = fit_rect(BOX, Size::new(50, 100), Fit::Cover);
assert_eq!(r.width, 100, "must span the full width");
assert_eq!(r.height, 200, "and overflow the height");
assert_eq!(r.y, 10 + (50 - 200) / 2, "overflow split evenly");
}
#[test]
fn contain_and_cover_preserve_aspect_within_rounding() {
for size in [Size::new(37, 91), Size::new(640, 480), Size::new(30, 80)] {
for fit in [Fit::Contain, Fit::Cover] {
let r = fit_rect(BOX, size, fit);
let derived = (size.height as i64 * r.width as i64 + size.width as i64 / 2)
/ size.width as i64;
assert!(
(derived - r.height as i64).abs() <= 1,
"{size:?} {fit:?} gave {r:?}"
);
}
}
}
#[test]
fn center_never_scales() {
let r = fit_rect(BOX, Size::new(30, 20), Fit::Center);
assert_eq!((r.width, r.height), (30, 20));
assert_eq!((r.x, r.y), (10 + 35, 10 + 15));
let r = fit_rect(BOX, Size::new(300, 200), Fit::Center);
assert_eq!((r.width, r.height), (300, 200));
assert_eq!(r.x, 10 + (100 - 300) / 2);
}
#[test]
fn fill_is_the_box() {
assert_eq!(fit_rect(BOX, Size::new(1, 1000), Fit::Fill), BOX);
}
#[test]
fn an_absurdly_thin_image_still_shows_a_sliver() {
let r = fit_rect(BOX, Size::new(1, 10_000), Fit::Contain);
assert_eq!(r.width, 1, "rounded to a sliver, not to nothing");
assert_eq!(r.height, 50);
}
}