use crate::event::Rect;
use crate::types::WidgetId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ImageSource {
Bytes(Vec<u8>),
Path(std::path::PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ImageFit {
#[default]
Contain,
Cover,
Fill,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Image {
pub id: WidgetId,
pub source: ImageSource,
pub intrinsic_size: Option<(u32, u32)>,
pub fit: ImageFit,
pub fallback_text: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImageLayout {
pub bounds: Rect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageHit {
Image,
Empty,
}
impl ImageLayout {
pub fn hit_test(&self, x: f32, y: f32) -> ImageHit {
let b = self.bounds;
if x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height {
ImageHit::Image
} else {
ImageHit::Empty
}
}
}
impl Image {
pub fn layout(&self, bounds: Rect) -> ImageLayout {
let Some((iw, ih)) = self.intrinsic_size else {
return ImageLayout { bounds };
};
if iw == 0 || ih == 0 || bounds.width <= 0.0 || bounds.height <= 0.0 {
return ImageLayout { bounds };
}
let iw = iw as f32;
let ih = ih as f32;
let target = match self.fit {
ImageFit::Fill => bounds,
ImageFit::None => Rect::new(bounds.x, bounds.y, iw, ih),
ImageFit::Contain => fit_within(bounds, iw, ih, true),
ImageFit::Cover => fit_within(bounds, iw, ih, false),
};
ImageLayout { bounds: target }
}
}
fn fit_within(bounds: Rect, iw: f32, ih: f32, contain: bool) -> Rect {
let scale_x = bounds.width / iw;
let scale_y = bounds.height / ih;
let scale = if contain {
scale_x.min(scale_y)
} else {
scale_x.max(scale_y)
};
let w = iw * scale;
let h = ih * scale;
let x = bounds.x + (bounds.width - w) / 2.0;
let y = bounds.y + (bounds.height - h) / 2.0;
Rect::new(x, y, w, h)
}
#[cfg(test)]
mod tests {
use super::*;
fn image(fit: ImageFit, intrinsic_size: Option<(u32, u32)>) -> Image {
Image {
id: WidgetId::new("img"),
source: ImageSource::Path("/tmp/logo.png".into()),
intrinsic_size,
fit,
fallback_text: "[LOGO]".into(),
}
}
#[test]
fn no_intrinsic_size_fills_bounds_regardless_of_fit() {
let bounds = Rect::new(0.0, 0.0, 40.0, 20.0);
let layout = image(ImageFit::Contain, None).layout(bounds);
assert_eq!(layout.bounds, bounds);
}
#[test]
fn fill_always_matches_bounds() {
let bounds = Rect::new(5.0, 5.0, 40.0, 20.0);
let layout = image(ImageFit::Fill, Some((100, 50))).layout(bounds);
assert_eq!(layout.bounds, bounds);
}
#[test]
fn none_paints_at_intrinsic_size_anchored_top_left() {
let bounds = Rect::new(5.0, 5.0, 40.0, 20.0);
let layout = image(ImageFit::None, Some((100, 50))).layout(bounds);
assert_eq!(layout.bounds, Rect::new(5.0, 5.0, 100.0, 50.0));
}
#[test]
fn contain_never_overflows_bounds_and_preserves_aspect_ratio() {
let bounds = Rect::new(0.0, 0.0, 40.0, 40.0);
let layout = image(ImageFit::Contain, Some((200, 100))).layout(bounds);
assert_eq!(layout.bounds.width, 40.0);
assert_eq!(layout.bounds.height, 20.0);
assert_eq!(layout.bounds.y, 10.0);
assert_eq!(layout.bounds.x, 0.0);
assert!(layout.bounds.width <= bounds.width);
assert!(layout.bounds.height <= bounds.height);
}
#[test]
fn cover_fills_bounds_on_the_shorter_axis_and_overflows_the_other() {
let bounds = Rect::new(0.0, 0.0, 40.0, 40.0);
let layout = image(ImageFit::Cover, Some((200, 100))).layout(bounds);
assert_eq!(layout.bounds.width, 80.0);
assert_eq!(layout.bounds.height, 40.0);
assert_eq!(layout.bounds.x, -20.0);
assert_eq!(layout.bounds.y, 0.0);
}
#[test]
fn zero_size_intrinsic_falls_back_to_bounds() {
let bounds = Rect::new(0.0, 0.0, 40.0, 20.0);
let layout = image(ImageFit::Contain, Some((0, 100))).layout(bounds);
assert_eq!(layout.bounds, bounds);
}
#[test]
fn hit_test_inside_resolved_bounds_is_image() {
let bounds = Rect::new(5.0, 5.0, 40.0, 20.0);
let layout = image(ImageFit::Fill, Some((100, 50))).layout(bounds);
assert_eq!(layout.hit_test(10.0, 10.0), ImageHit::Image);
}
#[test]
fn hit_test_outside_resolved_bounds_is_empty() {
let bounds = Rect::new(5.0, 5.0, 40.0, 20.0);
let layout = image(ImageFit::Fill, Some((100, 50))).layout(bounds);
assert_eq!(layout.hit_test(0.0, 0.0), ImageHit::Empty);
}
}