use std::hash::{Hash, Hasher};
use taffy::prelude::*;
use slate_renderer::Lpx;
use slate_renderer::scene::ImageInstance;
use crate::context::{LayoutCtx, PaintCtx, PrepaintCtx};
use crate::element::{Element, IntoElement, Sealed};
use crate::style::Style;
use crate::types::{
AccessibilityInfo, AccessibilityRole, Bounds, ElementId, LayoutId, NodeContext,
};
pub const MAX_IMAGE_DIM: u32 = 2048;
pub struct Image {
width: u32,
height: u32,
pixels: Vec<u8>,
content_hash: u64,
layout_style: Style,
user_key: Option<String>,
last_id: Option<ElementId>,
}
pub struct ImageLayoutState {
#[allow(dead_code)] node_id: taffy::NodeId,
}
pub struct ImagePaintState;
impl Image {
pub fn new(width: u32, height: u32, pixels: Vec<u8>) -> Self {
let expected_len = (width as usize) * (height as usize) * 4;
if width > MAX_IMAGE_DIM || height > MAX_IMAGE_DIM {
log::warn!(
"Image::new: dimensions {}x{} exceed MAX_IMAGE_DIM ({}); returning empty image",
width,
height,
MAX_IMAGE_DIM
);
return Self::default();
}
if pixels.len() != expected_len {
log::warn!(
"Image::new: pixels.len() ({}) != expected ({} = {}x{}x4); returning empty image",
pixels.len(),
expected_len,
width,
height
);
return Self::default();
}
let content_hash = {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
width.hash(&mut hasher);
height.hash(&mut hasher);
pixels.hash(&mut hasher);
hasher.finish()
};
Self {
width,
height,
pixels,
content_hash,
layout_style: Style::default(),
user_key: None,
last_id: None,
}
}
pub fn style(mut self, f: impl FnOnce(Style) -> Style) -> Self {
self.layout_style = f(self.layout_style);
self
}
pub fn key(mut self, k: impl Into<String>) -> Self {
self.user_key = Some(k.into());
self
}
#[inline]
pub fn width(&self) -> u32 {
self.width
}
#[inline]
pub fn height(&self) -> u32 {
self.height
}
#[inline]
pub fn content_hash(&self) -> u64 {
self.content_hash
}
}
impl Default for Image {
fn default() -> Self {
Self {
width: 0,
height: 0,
pixels: Vec::new(),
content_hash: 0,
layout_style: Style::default(),
user_key: None,
last_id: None,
}
}
}
impl Sealed for Image {}
impl Element for Image {
type LayoutState = ImageLayoutState;
type PaintState = ImagePaintState;
fn request_layout(&mut self, cx: &mut LayoutCtx) -> (LayoutId, Self::LayoutState) {
let mut taffy_style = taffy::Style::from(&self.layout_style);
if self.width > 0 && self.height > 0 {
taffy_style.size = taffy::Size {
width: Dimension::length(self.width as f32),
height: Dimension::length(self.height as f32),
};
}
let node_id = match cx.taffy.new_leaf(taffy_style) {
Ok(id) => id,
Err(e) => {
log::error!("Image: failed to create Taffy node: {e}; rendering empty");
match cx.taffy.new_leaf(taffy::Style::default()) {
Ok(id) => id,
Err(e2) => {
log::error!(
"Image: Taffy new_leaf also failed ({e2}) — pathological state"
);
taffy::NodeId::from(u64::MAX)
}
}
}
};
if let Err(e) = cx.taffy.set_node_context(node_id, Some(NodeContext::None)) {
log::error!("Image: failed to set node context: {e}; layout proceeds without context");
}
(LayoutId(node_id), ImageLayoutState { node_id })
}
fn prepaint(
&mut self,
bounds: Bounds,
_layout_state: &mut Self::LayoutState,
cx: &mut PrepaintCtx,
) -> Self::PaintState {
if let Some(k) = self.user_key.take() {
cx.set_next_key(k);
}
let element_id = cx.allocate_id::<Image>();
self.last_id = Some(element_id);
if let Some(info) = self.accessibility() {
cx.prepaint_node_open(element_id, bounds, info);
cx.prepaint_node_close();
}
ImagePaintState
}
fn paint(
&mut self,
bounds: Bounds,
_layout_state: &mut Self::LayoutState,
_paint_state: &mut Self::PaintState,
cx: &mut PaintCtx,
) {
if self.width == 0 || self.height == 0 {
return;
}
let alloc = cx.image_cache.upload_if_needed(
self.content_hash,
&self.pixels,
self.width,
self.height,
cx.image_atlas,
cx.queue,
);
if let Some(alloc) = alloc {
cx.scene.push_image(ImageInstance {
rect: [
Lpx(bounds.origin.x),
Lpx(bounds.origin.y),
Lpx(bounds.size.width),
Lpx(bounds.size.height),
],
uv_rect: alloc.uv_rect,
tint: [1.0, 1.0, 1.0, 1.0], });
}
}
fn accessibility(&self) -> Option<AccessibilityInfo> {
Some(AccessibilityInfo {
role: AccessibilityRole::Image,
..Default::default()
})
}
fn id(&self) -> Option<ElementId> {
self.last_id
}
fn paint_input_hash(&self, _bounds: Bounds) -> u64 {
self.content_hash
}
}
impl IntoElement for Image {
type Element = Self;
fn into_element(self) -> Self {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_valid_image() {
let pixels = vec![255u8; 10 * 10 * 4];
let img = Image::new(10, 10, pixels);
assert_eq!(img.width(), 10);
assert_eq!(img.height(), 10);
assert_ne!(img.content_hash(), 0);
}
#[test]
fn new_empty_on_invalid_length() {
let pixels = vec![255u8; 100]; let img = Image::new(10, 10, pixels);
assert_eq!(img.width(), 0);
assert_eq!(img.height(), 0);
}
#[test]
fn new_empty_on_oversized() {
let pixels = vec![255u8; 3000 * 100 * 4];
let img = Image::new(3000, 100, pixels); assert_eq!(img.width(), 0);
assert_eq!(img.height(), 0);
}
#[test]
fn default_is_empty() {
let img = Image::default();
assert_eq!(img.width(), 0);
assert_eq!(img.height(), 0);
assert_eq!(img.content_hash(), 0);
}
#[test]
fn content_hash_deterministic() {
let pixels = vec![128u8; 5 * 5 * 4];
let img1 = Image::new(5, 5, pixels.clone());
let img2 = Image::new(5, 5, pixels);
assert_eq!(img1.content_hash(), img2.content_hash());
}
#[test]
fn content_hash_differs_for_different_pixels() {
let pixels1 = vec![0u8; 5 * 5 * 4];
let pixels2 = vec![255u8; 5 * 5 * 4];
let img1 = Image::new(5, 5, pixels1);
let img2 = Image::new(5, 5, pixels2);
assert_ne!(img1.content_hash(), img2.content_hash());
}
}